New Upstream Release - vifm

Ready changes

Summary

Merged new upstream version: 0.13 (was: 0.12.1).

Resulting package

Built on 2023-08-22T19:27 (took 11m12s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases vifm-dbgsymapt install -t fresh-releases vifm

Lintian Result

Diff

diff --git a/AUTHORS b/AUTHORS
index 9917146..281a3d2 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -156,3 +156,11 @@ qsmodo contributed multiple patches.
 Hescalalu provided icon for the project (since July 2021).
 
 Completion for fish shell by Hoang Nguyen (a.k.a. FollieHiyuki).
+
+Alexandr Keyp (a.k.a. IAmKapuze) contributed improvements for :compare.
+
+高浩亮 (a.k.a. haolian9) enabled mouse handling.
+
+Zhipeng Xue fixed a couple of potential NULL dereferences.
+
+Rostislav Tolushkin (a.k.a. nullptr-deref) implemented :regedit command.
diff --git a/BUGS b/BUGS
index 5bf1304..16c570a 100644
--- a/BUGS
+++ b/BUGS
@@ -20,3 +20,5 @@
   doesn't work.
 * Access time of directories might not be preserved on OpenBSD on copying when
   'syscalls' is on, this is some subtle issue, because the code looks fine.
+* Search match highlighting has issues with handling hidden matched parts,
+  especially if 'tuioptions' contains "u".
diff --git a/ChangeLog b/ChangeLog
index 099a8da..62c50bd 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,273 @@
+0.13-beta to 0.13 (2023-04-04)
+
+	Made "withicase" and "withrcase" affect how files are sorted before
+	comparison, otherwise they might not produce the intended effect.
+
+	Reduced cursor flickering during incremental search in visual mode.  Patch
+	by filterfalse.
+
+	Fixed segfault on trying to expand "~username" path prefix with a huge
+	length of the user name.
+
+	Fixed user mappings replacing a mapping from Lua in an incomplete way
+	which could lead to assertion or some unpredictable behaviour.
+
+	Fixed map menus not listing mappings that accept a selector.
+
+	Fixed handling mouse events in normal and view modes in single pane mode.
+
+	Fixed :hideui not hiding anything on Windows.  Thanks to Ed Pavlov.
+
+	Fixed segfaults on changing sibling directory or previewing directories in
+	miller view while global sorting by groups is active.  Patch by filterfalse.
+
+	Fixed cl processing selection it was used previously on instead of file
+	under cursor when you run it twice in succession.
+
+	Fixed graphics from miller view preview remaining visible when displaying
+	dialogs, entering menus, switching tabs, activating more or file info
+	modes.
+
+	Fixed "rpreview:" of 'milleroptions' not being copied to new tabs.
+
+	Fixed selection not being stashed by do and dp.
+
+	Fixed memory issues inside ncurses when dialog is displayed in menu mode
+	after resizing the terminal.
+
+0.12.1 to 0.13-beta (2023-03-17)
+
+	Changed implementation of `:compare grouppaths` to juxtapose only files
+	with identical relative paths.  Patch by Alexandr Keyp (a.k.a. IAmKapuze).
+
+	Changed use of `$(filter-out)` in src/Makefile.am to `$(var:from=to)`
+	substitution to get rid of a warning on configuration.
+
+	Changed how pthread support is detected by configure script to handle more
+	cases.  Thanks to Markus Elfring (a.k.a. elfring).
+
+	Changed configure script to fail if neither perl nor vim is available
+	instead of failing to generate tags for Vim-style documentation.  Thanks
+	to Sergei Trofimovich (a.k.a. trofi).
+
+	Changed %N macro to also not start a process group for a command.  Thanks
+	to Oskar Grunning (a.k.a. sQVe).
+
+	Changed error reporting for some of the :commands such that now their
+	failures cause a dialog to appear on sourcing, previously those errors
+	were printed only to status bar.
+
+	Added "withicase" and "withrcase" to :compare that force ignoring and
+	respecting case respectively on comparing file names and paths.  Thanks to
+	Jose Riha (a.k.a. jose1711).
+
+	Added printing stats while in :compare mode.  Patch by Alexandr
+	Keyp (a.k.a. IAmKapuze).
+
+	Added "columncount:" value to 'lsoptions' to always display fixed number of
+	columns.  Thanks to Aleksandr Vysotskiy (a.k.a. loki1368).
+
+	Added show* arguments to :compare command to control/switch which sets of
+	files are displayed (toggling is done by :compare!).  Patch by Alexandr
+	Keyp (a.k.a. IAmKapuze).
+
+	Added builtin handling of mouse events.  Patch by 高浩亮 (a.k.a.
+	haolian9).  Thanks to ranousse, Sergei Shilovsky and user451421541757324.
+
+	Added 'mouse' option to control when mouse input is handled (not handled by
+	default).
+
+	Added input() builtin function to prompt user for input.  Thanks to Artur
+	F. (a.k.a. arturfabriciohahaedgy).
+
+	Added ETA to detailed progress dialog.  Thanks to Jose Riha (a.k.a.
+	jose1711).
+
+	Added `--plugins-dir` command-line option which allows specifying
+	additional places to look for plugins.  Thanks to 高浩亮 (a.k.a.
+	haolian9).
+
+	Added Ctrl-Y key to command-line mode.  It activates fast navigation that
+	allows entering deep paths by a series of searches for individual path
+	components.  Thanks to Henrik Holst (a.k.a. hholst80) and dmocek.
+
+	Added Ctrl-J key to command-line navigation.  It leaves the mode without
+	opening a file/directory.  Thanks to filterfalse.
+
+	Added Ctrl-O key to command-line navigation that goes to parent directory.
+
+	Added Ctrl-N/P keys to command-line navigation to move view cursor up/down.
+	Thanks to Henrik Holst (a.k.a. hholst80) and dmocek.
+
+	Added Arrows/Home/End/Page Up/Page Down keys to command-line navigation to
+	move view cursor.  Thanks to Henrik Holst (a.k.a. hholst80) and dmocek.
+
+	Added :amap, :anoremap and :aunmap commands to configure mappings in
+	navigation mode.  Thanks to Henrik Holst (a.k.a. hholst80) and dmocek.
+
+	Added 'navoptions' option to allow tweaking navigation mode a bit.  Thanks
+	to filterfalse.
+
+	Added "rpreview:files" to 'milleroptions'.  Thanks to aksr.
+
+	Added r key to :jobs menu, which reloads the list of jobs.  Thanks to
+	Sylwia Ptasinska (a.k.a. SylEleuth).
+
+	Added :regedit command for external editing of register contents.  Thanks
+	to Daniel J. Perry (BioBox).  Patch by Rostislav Tolushkin (a.k.a.
+	nullptr-deref).
+
+	Added additional User10..User20 highlight groups and corresponding
+	%10*-%20* macros.  Thanks to Sylwia Ptasinska (a.k.a. SylEleuth).
+
+	Added 'tabline' option to specify format of the tab line.
+
+	Added filereadable() builtin function mainly as a way to check file's
+	presence.
+
+	Reduced amount of memory consumed by `:compare groupids`.
+
+	Made `:compare bycontents` not bother reading content of files which have
+	unique size.
+
+	Provide basic instructions in the documentation on how mappings work.
+	Thanks to dmocek.
+
+	Extended do and dp keys to process selection.  Thanks to Mark S. (a.k.a.
+	Markuzcha).
+
+	Apply file highlighting to "ext" and "fileext" view columns.  Thanks to
+	aleksejrs.
+
+	Made instances running inside AppImage consider contents of `/etc/vifm`.
+	Thanks to aleksejrs.
+
+	Made Ctrl-E/Ctrl-Y scroll transposed ls-like view horizontally by one
+	column.
+
+	Install icons also to ${prefix}/share/icons.  It's not clear that using
+	${prefix}/share/pixmaps will always be handled properly.  Thanks to
+	Szilárd Andai.
+
+	Made feedback after pressing dd in :jobs more prominent.  Thanks to Sylwia
+	Ptasinska (a.k.a. SylEleuth).
+
+	Merged file conflict comparison dialog into the main conflict dialog.
+	Thanks to aleksejrs.
+
+	Made file conflict more concise.  Thanks to aleksejrs.
+
+	Update Default-256 to differentiate between more file types.  Thanks to
+	aleksejrs.
+
+	Don't move cursor on search failure during search with a count.  Patch by
+	filterfalse.
+
+	Specified a few more cases when to show a search message with search
+	highlighting turned on: if found a match, if wrapping is turned on, and in
+	visual mode.  Patch by filterfalse.
+
+	A regular search logic showing messages is applied to n/N.  Patch by
+	filterfalse.
+
+	Made aborting deletion abort the operation on the rest of files when
+	deleting multiple files.
+
+	Fixed segfault on trying to use pipe from Lua after its parent VifmJob
+	object was garbage-collected.  Thanks to PRESFIL.
+
+	Fixed check for broken symlinks in :edit command.
+
+	Fixed positioning hardware cursor in transposed ls-like view.
+
+	Fixed width of file names that don't fit into ls-like column being larger
+	than the width of other files.
+
+	Fixed ellipses being lost in side columns of miller view.
+
+	Fixed 'previewprg' not being respected on switching to view mode (regression
+	in 0.12-beta).  Thanks to Sitaram Chamarty.
+
+	Fixed $VIFM_APPDIR_ROOT being ignored by :help command, which made it not
+	work properly from AppImage.  Thanks to infinitewhileloop.
+
+	Fixed escaping of file paths when using 'vicmd' or 'vixcmd' to open a
+	file on Windows.  Thanks to Phil Runninger.
+
+	Fixed :locate never escaping its arguments (should be done unless the
+	first one starts with a dash).  This is a regression in 0.7.6.
+
+	Fixed trash directory being created at default location on :restart.
+
+	Fixed abort due to assertion failure on using zx normal mode key after
+	leaving tree in some cases.  Thanks to Mark S. (a.k.a. Markuzcha).
+
+	Fixed asynchronous previewing of symbolic links, which required manual
+	redraw.  Thanks to Alexandre Viau.
+
+	Fixed fish completion not completing paths for `--choose-files` and
+	`--choose-dir` options.
+
+	Fixed a "benign" data race on ga/gA.
+
+	Fixed error of :cunabbrev not being visible to the user.
+
+	Fixed segfault on passing invalid tag to :delbmarks.
+
+	Fixed :echo printing previous message when invoked without arguments.
+
+	Fixed Ctrl-E in visual mode not doing anything when view is scrolled to
+	its top.
+
+	Fixed 'milleroptions' not recovering value of "rpreview:" on error.
+
+	Fixed FUSE mounting assuming `2>` redirection is supported by the shell,
+	which isn't true at least for csh and tcsh.  Thanks to Evgeniy (a.k.a.
+	iron-udjin).
+
+	Fixed :media not redrawing on reloading ("r" key) and also wasn't
+	repositioning cursor properly when menu length shrinks as a result of a
+	reload.
+
+	Fixed stashing visual mode selection on transition from visual amend mode
+	to regular visual mode (av -> v).  Patch by filterfalse.
+
+	Fixed dropping normal selection on incremental search in visual amend
+	mode.  Patch by filterfalse.
+
+	Fixed input bar not being updated in visual mode if any status bar message
+	has been shown.  Patch by filterfalse.
+
+	Fixed error/prompt dialogs not reaching maximum allowed height.
+
+	Fixed "<<N more lines not shown>>" message in dialogs reporting wrong
+	number of lines and sometimes hiding a single line.
+
+	Fixed dialogs not handling non-latin characters well.
+
+	Fixed :source producing too many errors when specified path doesn't exist.
+
+	Fixed n/N not moving the cursor without prior search in visual mode.  Patch
+	by filterfalse.
+
+	Fixed showing wrong match number instead of error message on search in
+	visual mode.  Patch by filterfalse.
+
+	Fixed resetting 'hlsearch' during incremental search in visual mode.  Patch
+	by filterfalse.
+
+	Fixed dropping selected files on empty input during incremental search in
+	visual mode.  Patch by filterfalse.
+
+	Fixed resetting count to 1 during incremental search in normal mode.  Patch
+	by filterfalse.
+
+	Fixed description of %i macro in the documentation to mention that it runs
+	command in background.
+
+	Fixed the last search direction not being saved on search performed from
+	non-normal mode.  Patch by filterfalse.
+
 0.12.1-beta to 0.12.1 (2022-09-21)
 
 	Added shell completion for fish shell.  Patch by Hoang Nguyen (a.k.a.
@@ -329,7 +599,7 @@
 	current file correspondingly.  Thanks to j-xella.
 
 	Added 'previewoptions' option to allow tweaking graphics preview a bit.
-	Thanks to Joshua Jensch (a.k.a., patroclos) and flux242.
+	Thanks to Joshua Jensch (a.k.a. patroclos) and flux242.
 
 	Added "toptreestats" value to 'previewoptions' option, which makes stats
 	appear before the tree.  Patch by qsmodo.
@@ -3933,7 +4203,7 @@
 	Added handling of paths with backward slashes for :find/:locate/:grep/%M
 	menus on Windows.  Thanks to Robert Sarkozi.
 
-	Added sample light color scheme (provided by Daniel R., a.k.a. reicheltd).
+	Added sample light color scheme.  Patch by Daniel R. (a.k.a. reicheltd).
 
 	Added :lstrash command-line command, which displays list of files in trash.
 	Thanks to Sergei Shilovsky.
@@ -3946,7 +4216,7 @@
 	Svoboda, a.k.a. tex.
 
 	Added "type" key to the 'sort' option to allow controlling grouping of
-	directories.  Thanks to Daniel R., a.k.a. reicheltd.
+	directories.  Thanks to Daniel R. (a.k.a. reicheltd).
 
 	Added &option syntax for expressions (returns value of an option).
 
@@ -4025,8 +4295,8 @@
 	Fixed displaying of search messages in menus.
 
 	Fixed check for whether temporary files with list of file names is
-	changed during bulk rename operation.  Thanks to Daniel R., a.k.a.
-	reicheltd.
+	changed during bulk rename operation.  Thanks to Daniel R (a.k.a.
+	reicheltd).
 
 	Fixed reset of charset options (like 'cpoptions'), which could result in
 	segmentation faults.
@@ -4478,8 +4748,8 @@
 	Changed the way title of the permissions dialog is composed.
 
 	Fixed configuration on OS X and FreeBSD because of unavailable mntent.h
-	header and strverscmp() function (reported by Daniel R., a.k.a. r1chelt,
-	and Daniel Dettlaff, a.k.a. dmilith).
+	header and strverscmp() function.  Thanks to Daniel R. (a.k.a. r1chelt)
+	and Daniel Dettlaff (a.k.a. dmilith).
 
 	Fixed configuration when libmagic is not available, but selected (tried
 	to check its symbols).
diff --git a/ChangeLog.LuaAPI b/ChangeLog.LuaAPI
new file mode 100644
index 0000000..2907997
--- /dev/null
+++ b/ChangeLog.LuaAPI
@@ -0,0 +1,249 @@
+The API is experimental and having a change log is useful to have for
+adjusting plugins as the implementation changes.  During development of the
+API things should stay fairly stable to not cause breakage too often, but
+while stabilizing for v1.0 a bunch of breaking changes can happen at once.
+After that all changes will be documented in the regular ChangeLog.
+
+0.13-beta to 0.13 (2023-04-04)
+
+	Added VifmView:select() and VifmView:unselect() that select and unselect
+	entries by indexes.
+
+0.12.1 to 0.13-beta (2023-03-17)
+
+	Bumped API version to v0.1.0 due to bug fixes and various additions.
+
+	Additions to enabled standard libraries:
+	 * os.getenv()
+
+	New things Lua can be used for:
+	 * write a 'tabline' generator
+	 * running code before exit and when a file operation is done
+	 * starting background jobs to do something when they're over
+
+	Changed description of VifmTab:getview() without arguments to return
+	active pane of a global tab, which is what it was actually doing.
+
+	Added vifm.executable() that check for an executable file or command in
+	$PATH.
+
+	Added "onexit" callback for VifmJob.
+
+	Added vifm.input() that prompts user for input via command-line prompt.
+
+	Added events.listen() that registers a handler for an event.
+
+	Added "app.exit" event that occurs before Vifm exits normally.
+
+	Added "app.fsop" event that occurs after a successful execution of a
+	file-system operation.
+
+	Added VifmEntry.isdir field which is true if an entry is either a
+	directory or a symbolic link to a directory.
+
+	Added VifmView.custom table with information about custom file list.
+
+	Close Lua job streams when job is done.
+
+	Closed _G loophole a plugin could use to mess environment for other
+	plugins.
+
+	Protected against metatable manipulations in Lua.
+
+	Fixed incorrect path in Lua's editor handler on Windows.
+
+	Fixed VifmTab:getview() mistreating "pane" input field.
+
+	Fixed modules loaded via vifm.plugin.require() not having `vifm` defined.
+
+0.12 to 0.12.1 (2022-09-21)
+
+	Upgraded Lua from 5.4.3 to 5.4.4.
+
+	Additions to enabled standard libraries:
+	 * os.tmpname()
+
+	New things Lua can be used for:
+	 * fully integrate an arbitrary editor by using a handler in 'vicmd' or
+	   'vixcmd'
+	 * defining custom keys and selectors
+
+	Added vifm.escape() that escapes input to make it suitable for use in an
+	external command for current value of the 'shell' option.
+
+	Added vifm.run() that runs an external command similar to :!.
+
+	Added vifm.keys.add() that registers a user-defined key or selector.
+
+	Added vifm.sessions.current() that retrieves name of the current session.
+
+	Added vifm.tabs.get() that retrieves a tab.
+
+	Added vifm.tabs.getcount() that retrieves number of open tabs.
+
+	Added vifm.tabs.getcurrent() that retrieves index of current tab.
+
+	Added VifmEntry:mimetype() that gets a MIME type.
+
+	Added VifmTab:getlayout() that retrieves layout of a global tab or shared
+	TUI state for a pane tab.
+
+	Added VifmTab:getname() that retrieves name of the tab if it was set.
+
+	Added VifmTab:getview() that returns view managed by the tab.
+
+	Completors of a Lua-defined :command can now provide descriptions of
+	completion items.
+
+0.11 to 0.12 (2021-09-29)
+
+	The following standard libraries are enabled:
+	 * basic (core global functions)
+	 * table manipulation (`tbl` table)
+	 * package library (`require` global and `package` table)
+	 * string manipulation (`string` table)
+	 * input and output (`io` table)
+	 * mathematical functions (`math` table)
+	 * time subset of OS facilities (`clock`, `date`, `difftime` and `time`
+	   elements of `os` table)
+
+	 This is what one can do by using Lua at the moment:
+	  * commands with completion
+	  * save and reset options (e.g., some kind of presets)
+	  * custom columns
+	  * filetype/filextype/fileviewer/%q handlers
+	  * 'statusline' generator
+	  * starting background jobs that appear on a job bar
+
+	Added vifm.addcolumntype() that registers a new view column type to be
+	used in 'viewcolumns' option.
+
+	Added vifm.addhandler() that registers a new handler that will be
+	accessible in viewers.
+
+	Added vifm.currview() that retrieves a reference to current view.
+
+	Added vifm.otherview() that retrieves a reference to other view.
+
+	Added vifm.errordialog() that displays error dialog.
+
+	Added vifm.expand() that expands environment variables and macros in a
+	string.
+
+	Added vifm.fnamemodify() that changes path according to modifiers.
+
+	Added vifm.exists() that checks existence of a path without resolving
+	symbolic links.
+
+	Added vifm.makepath() that creates target path and missing intermediate
+	directories.
+
+	Added vifm.startjob() that launches an external command.
+
+	Added vifm.cmds.add() that registers a new :command of a kind that's
+	equivalent to builtin commands.
+
+	Added vifm.cmds.command() that registers a new :command that works exactly
+	as those registered using :command builtin command.
+
+	Added vifm.cmds.delcommand() that removes :command added by
+	vifm.cmds.command(), basically being an equivalent of :delcommand builtin
+	command.
+
+	Added vifm.opts.global table that provides access to global options.
+
+	Added vifm.plugin.name field that provides name of the plugin.
+
+	Added vifm.plugin.path field that provides full path to plugin's root
+	directory.
+
+	Added vifm.plugin.require() that serves as plugin-specific `require`,
+	which loads Lua modules relative to plugin's root.
+
+	Added vifm.plugins.all table that contains all plugins indexed by their
+	names.
+
+	Added vifm.sb.info() that displays a regular message on statusbar.
+
+	Added vifm.sb.error() that displays an error message on statusbar.
+
+	Added vifm.sb.quick() that displays a quick message on statusbar.
+
+	Added vifm.version.app.str field that provides version of Vifm as a
+	string.
+
+	Added vifm.version.api.major field that provides major version of Lua API.
+
+	Added vifm.version.api.minor field that provides minor version of Lua API.
+
+	Added vifm.version.api.patch field that provides patch version of Lua API.
+
+	Added vifm.version.api.has() that will check presence of features some
+	day.
+
+	Added vifm.version.api.atleast() that checks version of Lua API.
+
+	Added VifmEntry.classify table that describes name decorations.
+
+	Added VifmEntry.name field that provides name of the file.
+
+	Added VifmEntry.location field that provides location of the file.
+
+	Added VifmEntry.size field that provides size of the file in bytes.
+
+	Added VifmEntry.atime field that provides file access time (e.g., read,
+	executed).
+
+	Added VifmEntry.ctime field that provides file change time (changes in
+	metadata, like mode).
+
+	Added VifmEntry.mtime field that provides file modification time (when
+	file contents is changed).
+
+	Added VifmEntry.type field that provides type of the entry.
+
+	Added VifmEntry.folded field that shows whether this entry is folded.
+
+	Added VifmEntry.selected field that shows whether this entry is selected.
+
+	Added VifmEntry.match field that shows whether this entry is a search
+	match.
+
+	Added VifmEntry.matchstart field that provides start position of a
+	substring found in file's name.
+
+	Added VifmEntry.matchend field that provides end position of a substring
+	found in file's name.
+
+	Added VifmEntry:gettarget() that gets target of a symbolic link (not to be
+	confused with real path resolution).
+
+	Added VifmJob:wait() that waits for the job to finish.
+
+	Added VifmJob:exitcode() that retrieves exit code of the job.
+
+	Added VifmJob:stdin() that retrieves stream associated with standard input
+	of the job.
+
+	Added VifmJob:stdout() that retrieves stream associated with standard
+	output of the job.
+
+	Added VifmJob:errors() that retrieves data collected from error stream of
+	the job.
+
+	Added VifmView.locopts table of location-specific values of view-specific
+	options.
+
+	Added VifmView.viewopts table of "global" values of view-specific options.
+
+	Added VifmView.currententry field that provides index of the current
+	entry.
+
+	Added VifmView.cwd field that provides location of the current view.
+
+	Added VifmView.entrycount field that provides number of entries in the
+	view.
+
+	Added VifmView:cd() that changes location of the view.
+
+	Added VifmView:entry() that retrieves an entry by index.
diff --git a/HACKING.md b/HACKING.md
index ba3b226..b790e34 100644
--- a/HACKING.md
+++ b/HACKING.md
@@ -3,21 +3,34 @@
 It's a good idea to describe what it is you would like to implement before
 starting it.  The are multiple reasons for this:
 
- - new feature might interact with other feature in ways you don't expect;
+ - new feature might interact with other features in unexpected ways;
  - not all ways of implementing a feature are equal, some might be preferred
    just for the sake of keeping things consistent;
  - the code base is quite large and stuff that's already there might save you
    some time.
 
-Changes can be sent as a pull request on [github][github] or as a patch via
-[email][email].
+Changes can be sent as a pull request on [github] or as a patch via [email].
 
-Some information on development is available on the [wiki][wiki].
+Some information on development is available on the [wiki].
 
 [github]: https://github.com/vifm/vifm/pulls
 [email]: mailto:xaizek@posteo.net
 [wiki]: https://wiki.vifm.info/index.php?title=Development#Code_repositories
 
+## Setup for development ##
+
+To avoid possible issues with `autoconf` caused by skewed timestamps after
+`git clone` or `git checkout` execute `scripts/fix-timestamps`.
+
+Configure the project with `--enable-developer` flag to compile in debug mode
+and with `-Werror` (there is also `--enable-werror` that doesn't enable debug
+mode).  Undo this if it breaks the build from `master` because of unexpected
+warnings.
+
+After making changes, don't forget to run tests (see below) to make sure they
+pass.  It might be a good idea to start by running tests as well to see that
+they succeed without any changes.
+
 ## Tests ##
 
 Tests are available and can be run with `make check` in the root, `src/` or
@@ -25,6 +38,21 @@ Tests are available and can be run with `make check` in the root, `src/` or
 specific tests in several different modes, see comment inside the `Makefile`
 there for instructions.
 
+## ChangeLog format ##
+
+Releases are listed in newest to oldest order.  Beta versions are considered to
+be separate releases, otherwise one would have to edit entries rather than add
+new ones on recording changes done since beta.
+
+Within each release entries are organized in groups of 4 kinds in this order:
+ 1. Changed ...
+ 2. Added ...
+ 3. Everything else
+ 4. Fixed ...
+
+A new entry is added at the end of a group, resulting in a chronological order
+of changes of each kind.
+
 ## Package contents ##
 
     .
@@ -105,9 +133,11 @@ there for instructions.
     |  |
     |  |-- lua/ - Lua interface
     |  |  |
-    |  |  |-- lua/ - Lua 5.4.2 sources
+    |  |  |-- lua/ - Lua 5.4.4 sources
     |  |  |-- common.c - common code for Lua API implementation
+    |  |  |-- vifm.c - implementation of `vifm`
     |  |  |-- vifm_cmds.c - implementation of `vifm.cmds`
+    |  |  |-- vifm_events.c - implementation of `vifm.events`
     |  |  |-- vifm_handlers.c - implementation of `vifm.addhandler`
     |  |  |-- vifm_tabs.c - implementation of `vifm.tabs`
     |  |  |-- vifm_viewcolumns.c - implementation of `vifm.addcolumntype`
@@ -116,6 +146,7 @@ there for instructions.
     |  |  |-- vifmtab.c - implementation of VifmTab
     |  |  |-- vifmview.c - implementation of VifmView
     |  |  |-- vlua.c - main Lua interface unit
+    |  |  |-- vlua_cbacks.c - manager of callback functions
     |  |  `-- vlua_state.c - management of state of vlua.c unit
     |  |
     |  |-- menus/ - implementation of all menus
diff --git a/INSTALL b/INSTALL
index d4521a9..f3276ce 100644
--- a/INSTALL
+++ b/INSTALL
@@ -3,22 +3,17 @@ Installation
 
 On *nix you need:
 
-    A working version of ncursesw built as a shared library (*.so files,
-    enabled by the --with-shared switch of ncurses' configure script) and
-    an ascii compliant terminal.  ncurses can be static, but that will
-    require providing appropriate options for linker (thus might not work
-    out of the box).
+    A working version of ncursesw-compatible library.
 
     Hints for building on Debian-based systems (Debian/Ubuntu/Linux Mint):
      * ncurses package you need to build vifm is called "libncursesw5-dev"
+     * also install either perl interpreter or vim to generate tags (done
+       during build)
      * additional (optional) tools used in sample vifmrc:
        - sudo aptitude install sshfs curlftpfs fuse fuse-zip fusefat fuseiso
        - for a support of .rar-archive files see:
          https://gist.github.com/enberg/84710b619bdb0b10e945
 
-    For best user-experience with Vim plugins availability of either perl
-    interpreter or vim executable is needed to automatically generate tags.
-
 *nix Installation (Cygwin emulates *nix environment, so follow these steps
 when building with Cygwin):
 
@@ -82,13 +77,12 @@ when building with Cygwin):
     vifmrc files from the data/ directory to ~/.vifm/.
 
     After you start vifm for the first time, you can edit the configuration
-    file.  It will be at ~/.vifm/vifmrc.  See help for description of other
-    files in ~/.vifm directory.
+    file.  It will be at ~/.config/vifm/vifmrc unless ~/.vifm/ exists.  See
+    help for description of other files in configuration directory.
 
 On OS X you need:
 
-    Get and install libscursesw or libncursesw as shared libraries (*.so
-    files, enabled by the --with-shared switch of ncurses' configure script).
+    Get and install libscursesw or libncursesw.
 
     Proceed like in *nix Installation section.
 
diff --git a/Makefile.am b/Makefile.am
index 590b2bc..a85a141 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -2,7 +2,7 @@
 
 SUBDIRS = src
 
-EXTRA_DIST = COPYING.3party FAQ BUGS patches pkgs tests
+EXTRA_DIST = COPYING.3party ChangeLog.LuaAPI FAQ BUGS patches pkgs tests
 dist-hook:
 	make -C "$(distdir)/tests" clean
 	rm -f "$(distdir)/tests/.in.vim"
diff --git a/Makefile.in b/Makefile.in
index c46b34f..dfbf9e7 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -91,6 +91,7 @@ subdir = .
 ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
 am__aclocal_m4_deps =  \
 	$(top_srcdir)/build-aux/m4/ax_check_compile_flag.m4 \
+	$(top_srcdir)/build-aux/m4/ax_pthread.m4 \
 	$(top_srcdir)/configure.ac
 am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
 	$(ACLOCAL_M4)
@@ -260,7 +261,12 @@ PACKAGE_URL = @PACKAGE_URL@
 PACKAGE_VERSION = @PACKAGE_VERSION@
 PATH_SEPARATOR = @PATH_SEPARATOR@
 PERL_PROG = @PERL_PROG@
+PTHREAD_CC = @PTHREAD_CC@
+PTHREAD_CFLAGS = @PTHREAD_CFLAGS@
+PTHREAD_CXX = @PTHREAD_CXX@
+PTHREAD_LIBS = @PTHREAD_LIBS@
 SANITIZERS_CFLAGS = @SANITIZERS_CFLAGS@
+SED = @SED@
 SED_PROG = @SED_PROG@
 SET_MAKE = @SET_MAKE@
 SHELL = @SHELL@
@@ -278,6 +284,7 @@ am__leading_dot = @am__leading_dot@
 am__quote = @am__quote@
 am__tar = @am__tar@
 am__untar = @am__untar@
+ax_pthread_config = @ax_pthread_config@
 bindir = @bindir@
 build = @build@
 build_alias = @build_alias@
@@ -319,7 +326,7 @@ top_build_prefix = @top_build_prefix@
 top_builddir = @top_builddir@
 top_srcdir = @top_srcdir@
 SUBDIRS = src
-EXTRA_DIST = COPYING.3party FAQ BUGS patches pkgs tests
+EXTRA_DIST = COPYING.3party ChangeLog.LuaAPI FAQ BUGS patches pkgs tests
 all: all-recursive
 
 .SUFFIXES:
diff --git a/NEWS b/NEWS
index 35218bb..f2b4474 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,414 @@
+04 April 2023
+=============
+
+Vifm v0.13
+----------
+
+Thanks to everyone who tried out the beta.
+
+Highlights:
+
+ - Faster file-system navigation with a new searching/filtering submode.
+
+   Hitting `<c-y>` after `/` or `=` allows finding and opening consecutive path
+   components without leaving command-line mode.  In this mode keys like
+   `<c-o>`/`<c-n>`/`<c-p>`/`<left>`/`<home>`/etc. help to refine cursor
+   position or go to parent directory if necessary.
+
+ - More interactive :compare.
+
+   You can now see stats about file comparison on the status bar and have an
+   option to easily re-run the comparison while toggling visibility of some of
+   the groups.
+
+   Also, default file alignment is more natural now.  `do` and `dp` handle
+   selection.  You can force ignoring or respecting case in paths.  Performance
+   and memory consumption have been improved in various cases.
+
+ - Mouse support and TUI improvements.
+
+   Once mouse support is enabled, you should be able to perform simple browsing
+   with just your mouse for those cases when it's more convenient.
+
+   Conflict dialog now always presents basic file metadata for comparison.
+   Detailed progress dialog has ETA.  Ls-like view can now have a fixed number
+   of columns.
+
+ - Other.
+
+   Formatting of tabs and entry selection in Lua.  Changing register's content
+   via an editor.  Explicitly prompting user for input via input() function.
+
+ - Fixes.
+
+   v0.12.1 was supposed to improve escaping on Windows, but it simultaneously
+   made one old bug more prominent, effectively making things much worse in
+   some cases.  This release remedies that.
+
+   %N macro has been adjusted to fix integration with image preview of Kitty
+   v0.27+.
+
+   Lots of fixes related to search and various combinations of 'hlsearch',
+   'incsearch' in different modes.
+
+Normal and visual modes:
+ - made Ctrl-E/Ctrl-Y scroll transposed ls-like view horizontally by one column;
+ - extended do and dp keys to process selection (thanks to Mark S., a.k.a.
+   Markuzcha).
+
+:set command and options:
+ - added 'mouse' option to control when mouse input is handled (not handled by
+   default);
+ - added 'navoptions' option to allow tweaking navigation mode a bit (thanks to
+   filterfalse);
+ - added 'tabline' option to specify format of the tab line;
+ - added "columncount:" value to 'lsoptions' to always display fixed number of
+   columns (thanks to Aleksandr Vysotskiy, a.k.a. loki1368);
+ - added "rpreview:files" to 'milleroptions' (thanks to aksr).
+
+Command-line mode:
+ - changed error reporting for some of the :commands such that now their
+   failures cause a dialog to appear on sourcing, previously those errors were
+   printed only to status bar;
+ - changed implementation of `:compare grouppaths` to juxtapose only files with
+   identical relative paths (patch by Alexandr Keyp, a.k.a. IAmKapuze);
+ - added "withicase" and "withrcase" to :compare that force ignoring and
+   respecting case respectively on comparing file names and paths (thanks to
+   Jose Riha, a.k.a. jose1711);
+ - added show* arguments to :compare command to control/switch which sets of
+   files are displayed (toggling is done by :compare!) (patch by Alexandr Keyp,
+   a.k.a. IAmKapuze);
+ - added :amap, :anoremap and :aunmap commands to configure mappings in
+   navigation mode (thanks to Henrik Holst, a.k.a. hholst80, and dmocek);
+ - added :regedit command for external editing of register contents (thanks to
+   Daniel J. Perry, a.k.a. BioBox; patch by Rostislav Tolushkin, a.k.a.
+   nullptr-deref);
+ - added Ctrl-Y key to command-line mode.  It activates fast navigation that
+   allows entering deep paths by a series of searches for individual path
+   components (thanks to Henrik Holst, a.k.a. hholst80, and dmocek);
+ - added Ctrl-J key to command-line navigation.  It leaves the mode without
+   opening a file/directory (thanks to filterfalse);
+ - added Ctrl-O key to command-line navigation that goes to parent directory;
+ - added Ctrl-N/P keys to command-line navigation to move view cursor up/down
+   (thanks to Henrik Holst, a.k.a. hholst80, and dmocek);
+ - added Arrows/Home/End/Page Up/Page Down keys to command-line navigation to
+   move view cursor (thanks to Henrik Holst, a.k.a. hholst80, and dmocek).
+
+Core:
+ - added printing stats while in :compare mode (patch by Alexandr Keyp, a.k.a.
+   IAmKapuze);
+ - don't move cursor on search failure during search with a count (patch by
+   filterfalse);
+ - specified a few more cases when to show a search message with search
+   highlighting turned on: if found a match, if wrapping is turned on, and in
+   visual mode (patch by filterfalse);
+ - a regular search logic showing messages is applied to n/N (patch by
+   filterfalse).
+
+File operations:
+ - made aborting deletion abort the operation on the rest of files when
+   deleting multiple files.
+
+Macros:
+ - changed %N macro to also not start a process group for a command (thanks to
+   Oskar Grunning, a.k.a. sQVe).
+
+Scripting:
+ - added input() builtin function to prompt user for input (thanks to Artur F.,
+   a.k.a. arturfabriciohahaedgy);
+ - added filereadable() builtin function mainly as a way to check file's
+   presence.
+
+Menus and dialogs:
+ - added r key to :jobs menu, which reloads the list of jobs (thanks to Sylwia
+   Ptasinska, a.k.a. SylEleuth);
+ - made feedback after pressing dd in :jobs more prominent (thanks to Sylwia
+   Ptasinska, a.k.a. SylEleuth).
+
+TUI (Text User Interface):
+ - added builtin handling of mouse events (thanks to ranousse, Sergei Shilovsky
+   and user451421541757324; patch by 高浩亮, a.k.a. haolian9);
+ - added ETA to detailed progress dialog (thanks to Jose Riha, a.k.a.
+   jose1711);
+ - apply file highlighting to "ext" and "fileext" view columns (thanks to
+   aleksejrs);
+ - merged file conflict comparison dialog into the main conflict dialog (thanks
+   to aleksejrs);
+ - made file conflict more concise (thanks to aleksejrs).
+
+Color schemes:
+ - added additional User10..User20 highlight groups and corresponding %10*-%20*
+   macros (thanks to Sylwia Ptasinska, a.k.a. SylEleuth);
+ - update Default-256 to differentiate between more file types (thanks to
+   aleksejrs).
+
+Invocation:
+ - added `--plugins-dir` command-line option which allows specifying additional
+   places to look for plugins (thanks to 高浩亮, a.k.a. haolian9).
+
+Performance:
+ - reduced amount of memory consumed by `:compare groupids`;
+ - made `:compare bycontents` not bother reading content of files which have
+   unique size.
+
+Documentation:
+ - provide basic instructions in the documentation on how mappings work (thanks
+   to dmocek);
+ - fixed description of %i macro in the documentation to mention that it runs
+   command in background.
+
+Packaging:
+ - changed use of `$(filter-out)` in src/Makefile.am to `$(var:from=to)`
+   substitution to get rid of a warning on configuration;
+ - changed how pthread support is detected by configure script to handle more
+   cases (thanks to Markus Elfring, a.k.a. elfring);
+ - changed configure script to fail if neither perl nor vim is available
+   instead of failing to generate tags for Vim-style documentation (thanks to
+   Sergei Trofimovich, a.k.a. trofi);
+ - install icons also to ${prefix}/share/icons.  It's not clear that using
+   ${prefix}/share/pixmaps will always be handled properly (thanks to Szilárd
+   Andai).
+
+Integration:
+ - made instances running inside AppImage consider contents of `/etc/vifm`
+   (thanks to aleksejrs);
+ - fixed $VIFM_APPDIR_ROOT being ignored by :help command, which made it not
+   work properly from AppImage (thanks to infinitewhileloop).
+
+Only on Windows:
+ - fixed escaping of file paths when using 'vicmd' or 'vixcmd' to open a file
+   on Windows (thanks to Phil Runninger).
+
+Noteworthy fixes:
+ - fixed abort due to assertion failure on using zx normal mode key after
+   leaving tree in some cases (thanks to Mark S., a.k.a. Markuzcha);
+ - fixed asynchronous previewing of symbolic links, which required manual
+   redraw (thanks to Alexandre Viau);
+ - fixed FUSE mounting assuming `2>` redirection is supported by the shell,
+   which isn't true at least for csh and tcsh (thanks to Evgeniy, a.k.a.
+   iron-udjin);
+ - fixed dialogs not handling non-latin characters well;
+ - fixed :locate never escaping its arguments (should be done unless the first
+   one starts with a dash).  This is a regression in 0.7.6;
+ - fixed n/N not moving the cursor without prior search (patch by filterfalse);
+ - fixed resetting 'hlsearch' during incremental search in visual mode (patch
+   by filterfalse);
+ - fixed dropping selected files on empty input during incremental search in
+   visual mode when 'hlsearch' is set (patch by filterfalse);
+ - fixed segfault on trying to use a pipe from Lua after its parent VifmJob
+   object was garbage-collected (thanks to PRESFIL);
+ - fixed 'previewprg' not being respected on switching to view mode (regression
+   in 0.12-beta) (thanks to Sitaram Chamarty).
+
+See change log for the full list of changes and by whom they were suggested or
+implemented.
+
+17 March 2023
+=============
+
+Vifm v0.13 beta
+---------------
+
+The beta stage will last about two weeks.  In case any serious bugs are found
+during this period, another beta version might be released.
+
+Highlights:
+
+ - Faster file-system navigation with a new searching/filtering submode.
+
+   Hitting `<c-y>` after `/` or `=` allows finding and opening consecutive path
+   components without leaving command-line mode.  In this mode keys like
+   `<c-o>`/`<c-n>`/`<c-p>`/`<left>`/`<home>`/etc. help to refine cursor
+   position or go to parent directory if necessary.
+
+ - More interactive :compare.
+
+   You can now see stats about file comparison on the status bar and have an
+   option to easily re-run the comparison while toggling visibility of some of
+   the groups.
+
+   Also, default file alignment is more natural now.  `do` and `dp` handle
+   selection.  You can force ignoring or respecting case in paths.  Performance
+   and memory consumption have been improved in various cases.
+
+ - Mouse support and TUI improvements.
+
+   Once mouse support is enabled, you should be able to perform simple browsing
+   with just your mouse for those cases when it's more convenient.
+
+   Conflict dialog now always presents basic file metadata for comparison.
+   Detailed progress dialog has ETA.  Ls-like view can now have a fixed number
+   of columns.
+
+ - Other.
+
+   Formatting of tabs in Lua.  Changing register's content via an editor.
+   Explicitly prompting user for input via input() function.
+
+ - Fixes.
+
+   v0.12.1 was supposed to improve escaping on Windows, but it simultaneously
+   made one old bug more prominent, effectively making things much worse in
+   some cases.  This release remedies that.
+
+   %N macro has been adjusted to fix integration with image preview of Kitty
+   v0.27+.
+
+   Lots of fixes related to search and various combinations of 'hlsearch',
+   'incsearch' in different modes.
+
+Normal and visual modes:
+ - made Ctrl-E/Ctrl-Y scroll transposed ls-like view horizontally by one column;
+ - extended do and dp keys to process selection (thanks to Mark S., a.k.a.
+   Markuzcha).
+
+:set command and options:
+ - added 'mouse' option to control when mouse input is handled (not handled by
+   default);
+ - added 'navoptions' option to allow tweaking navigation mode a bit (thanks to
+   filterfalse);
+ - added 'tabline' option to specify format of the tab line;
+ - added "columncount:" value to 'lsoptions' to always display fixed number of
+   columns (thanks to Aleksandr Vysotskiy, a.k.a. loki1368);
+ - added "rpreview:files" to 'milleroptions' (thanks to aksr).
+
+Command-line mode:
+ - changed error reporting for some of the :commands such that now their
+   failures cause a dialog to appear on sourcing, previously those errors were
+   printed only to status bar;
+ - changed implementation of `:compare grouppaths` to juxtapose only files with
+   identical relative paths (patch by Alexandr Keyp, a.k.a. IAmKapuze);
+ - added "withicase" and "withrcase" to :compare that force ignoring and
+   respecting case respectively on comparing file names and paths (thanks to
+   Jose Riha, a.k.a. jose1711);
+ - added show* arguments to :compare command to control/switch which sets of
+   files are displayed (toggling is done by :compare!) (patch by Alexandr Keyp,
+   a.k.a. IAmKapuze);
+ - added :amap, :anoremap and :aunmap commands to configure mappings in
+   navigation mode (thanks to Henrik Holst, a.k.a. hholst80, and dmocek);
+ - added :regedit command for external editing of register contents (thanks to
+   Daniel J. Perry, a.k.a. BioBox; patch by Rostislav Tolushkin, a.k.a.
+   nullptr-deref);
+ - added Ctrl-Y key to command-line mode.  It activates fast navigation that
+   allows entering deep paths by a series of searches for individual path
+   components (thanks to Henrik Holst, a.k.a. hholst80, and dmocek);
+ - added Ctrl-J key to command-line navigation.  It leaves the mode without
+   opening a file/directory (thanks to filterfalse);
+ - added Ctrl-O key to command-line navigation that goes to parent directory;
+ - added Ctrl-N/P keys to command-line navigation to move view cursor up/down
+   (thanks to Henrik Holst, a.k.a. hholst80, and dmocek);
+ - added Arrows/Home/End/Page Up/Page Down keys to command-line navigation to
+   move view cursor (thanks to Henrik Holst, a.k.a. hholst80, and dmocek).
+
+Core:
+ - added printing stats while in :compare mode (patch by Alexandr Keyp, a.k.a.
+   IAmKapuze);
+ - don't move cursor on search failure during search with a count (patch by
+   filterfalse);
+ - specified a few more cases when to show a search message with search
+   highlighting turned on: if found a match, if wrapping is turned on, and in
+   visual mode (patch by filterfalse);
+ - a regular search logic showing messages is applied to n/N (patch by
+   filterfalse).
+
+File operations:
+ - made aborting deletion abort the operation on the rest of files when
+   deleting multiple files.
+
+Macros:
+ - changed %N macro to also not start a process group for a command (thanks to
+   Oskar Grunning, a.k.a. sQVe).
+
+Scripting:
+ - added input() builtin function to prompt user for input (thanks to Artur F.,
+   a.k.a. arturfabriciohahaedgy);
+ - added filereadable() builtin function mainly as a way to check file's
+   presence.
+
+Menus and dialogs:
+ - added r key to :jobs menu, which reloads the list of jobs (thanks to Sylwia
+   Ptasinska, a.k.a. SylEleuth);
+ - made feedback after pressing dd in :jobs more prominent (thanks to Sylwia
+   Ptasinska, a.k.a. SylEleuth).
+
+TUI (Text User Interface):
+ - added builtin handling of mouse events (thanks to ranousse, Sergei Shilovsky
+   and user451421541757324; patch by 高浩亮, a.k.a. haolian9);
+ - added ETA to detailed progress dialog (thanks to Jose Riha, a.k.a.
+   jose1711);
+ - apply file highlighting to "ext" and "fileext" view columns (thanks to
+   aleksejrs);
+ - merged file conflict comparison dialog into the main conflict dialog (thanks
+   to aleksejrs);
+ - made file conflict more concise (thanks to aleksejrs).
+
+Color schemes:
+ - added additional User10..User20 highlight groups and corresponding %10*-%20*
+   macros (thanks to Sylwia Ptasinska, a.k.a. SylEleuth);
+ - update Default-256 to differentiate between more file types (thanks to
+   aleksejrs).
+
+Invocation:
+ - added `--plugins-dir` command-line option which allows specifying additional
+   places to look for plugins (thanks to 高浩亮, a.k.a. haolian9).
+
+Performance:
+ - reduced amount of memory consumed by `:compare groupids`;
+ - made `:compare bycontents` not bother reading content of files which have
+   unique size.
+
+Documentation:
+ - provide basic instructions in the documentation on how mappings work (thanks
+   to dmocek);
+ - fixed description of %i macro in the documentation to mention that it runs
+   command in background.
+
+Packaging:
+ - changed use of `$(filter-out)` in src/Makefile.am to `$(var:from=to)`
+   substitution to get rid of a warning on configuration;
+ - changed how pthread support is detected by configure script to handle more
+   cases (thanks to Markus Elfring, a.k.a. elfring);
+ - changed configure script to fail if neither perl nor vim is available
+   instead of failing to generate tags for Vim-style documentation (thanks to
+   Sergei Trofimovich, a.k.a. trofi);
+ - install icons also to ${prefix}/share/icons.  It's not clear that using
+   ${prefix}/share/pixmaps will always be handled properly (thanks to Szilárd
+   Andai).
+
+Integration:
+ - made instances running inside AppImage consider contents of `/etc/vifm`
+   (thanks to aleksejrs);
+ - fixed $VIFM_APPDIR_ROOT being ignored by :help command, which made it not
+   work properly from AppImage (thanks to infinitewhileloop).
+
+Only on Windows:
+ - fixed escaping of file paths when using 'vicmd' or 'vixcmd' to open a file
+   on Windows (thanks to Phil Runninger).
+
+Noteworthy fixes:
+ - fixed abort due to assertion failure on using zx normal mode key after
+   leaving tree in some cases (thanks to Mark S., a.k.a. Markuzcha);
+ - fixed asynchronous previewing of symbolic links, which required manual
+   redraw (thanks to Alexandre Viau);
+ - fixed FUSE mounting assuming `2>` redirection is supported by the shell,
+   which isn't true at least for csh and tcsh (thanks to Evgeniy, a.k.a.
+   iron-udjin);
+ - fixed dialogs not handling non-latin characters well;
+ - fixed :locate never escaping its arguments (should be done unless the first
+   one starts with a dash).  This is a regression in 0.7.6;
+ - fixed n/N not moving the cursor without prior search (patch by filterfalse);
+ - fixed resetting 'hlsearch' during incremental search in visual mode (patch
+   by filterfalse);
+ - fixed dropping selected files on empty input during incremental search in
+   visual mode when 'hlsearch' is set (patch by filterfalse);
+ - fixed segfault on trying to use a pipe from Lua after its parent VifmJob
+   object was garbage-collected (thanks to PRESFIL);
+ - fixed 'previewprg' not being respected on switching to view mode (regression
+   in 0.12-beta) (thanks to Sitaram Chamarty).
+
+See change log for the full list of changes and by whom they were suggested or
+implemented.
+
 21 September 2022
 =================
 
diff --git a/README b/README
index 5b3d3de..b5d96d9 100644
--- a/README
+++ b/README
@@ -1,9 +1,9 @@
 Vifm - Vim-like file manager
-2001 - 2022
+2001 - 2023
 
-Version: 0.12.1
+Version: 0.13
 
-This file last updated: 21 September 2022
+This file last updated: 04 April 2023
 
 Brief Description
 
diff --git a/README.md b/README.md
index 873c727..fdc59e1 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
 
 ![Vifm logo](data/graphics/vifm-96px.png)
 
-[![][AA]][A]  [![][FF]][F]  [![][UU]][U]
+[![][AA]][A]  [![][FF]][F]  [![][UU]][U]  [![][SS]][S]
 
-_Latest release is 0.12.1.  This file last updated on 21 September 2022._
+_Latest release is 0.13.  This file last updated on 04 April 2023._
 
 ## Brief Description ##
 
@@ -87,3 +87,5 @@ GNU General Public License, version 2 or later.
 [F]: http://ci.vifm.info/
 [UU]: http://cov.vifm.info/badges/svg/master
 [U]: http://cov.vifm.info/branches/master
+[SS]: https://scan.coverity.com/projects/699/badge.svg
+[S]: https://scan.coverity.com/projects/vifm-vifm
diff --git a/THANKS b/THANKS
index 8455719..7556850 100644
--- a/THANKS
+++ b/THANKS
@@ -1,8 +1,14 @@
-People named in this file helped make vifm better, less buggy and more portable
-by providing their ideas, suggestions, bug reports and pieces of code.
-
+People named in this file helped make Vifm better, less buggy and more portable
+by providing their ideas, suggestions, bug reports and time for testing.
 Names (or nick names) are in alphabetical order.
 
+Special thanks go to these individuals who actively participated in development
+and testing of large number of features:
+ * Alexandre Viau
+ * Colin Cartade
+ * filterfalse
+ * Svyatoslav Mishyn (juef)
+
 45jqlakjrf87ayte7hy34ter4nguijauzl4eitk
 702b
 Abdó Roig-Maranges (aroig)
@@ -10,8 +16,10 @@ aca
 afsheenb
 Afz
 agguser
+aksr
 Alborz Jafari
 Alejandro Pulver (alepulver)
+Aleksandr Vysotskiy (loki1368)
 aleksejrs
 Alexandru Geana (alegen)
 Allan Neal (akneal)
@@ -21,6 +29,7 @@ Andrew Lannan
 Andrew Savchenko
 Anton Gepting
 Anton Kochkov (XVilka)
+Artur F. (arturfabriciohahaedgy)
 Artur Shaik (artur-shaik)
 astrell
 AtomToast
@@ -34,7 +43,6 @@ Behrooz
 Ben Boeckel (mathstuf)
 Ben Lu (ayroblu)
 Bernhard Grotz
-Daniel J. Perry (BioBox)
 blurm
 Branislav Gerazov
 bratekarate
@@ -62,6 +70,7 @@ dalvand
 Damian Ariel Perticone
 dancread
 Daniel Dettlaff (dmilith)
+Daniel J. Perry (BioBox)
 Daniel Mueller (d-e-s-o)
 Daniel Polanco (dlpolanco)
 Daniel R. (r1chelt)
@@ -73,15 +82,17 @@ DieSpinne
 dikiy
 Diogo Lemos (dmlemos)
 Dmitry Frank (dimonomid)
+dmocek
 eco0414
+Ed Pavlov
 Egor Gumenyuk (boo1ean)
 einhander
 elricbk
 emarsk
 emorozov
+Evgeniy (iron-udjin)
 Factorial Prime
 Fang (peromage)
-filterfalse
 Flaviu Tamas (flaviut)
 Florian Baumann (derflob)
 flux242
@@ -92,6 +103,7 @@ gcmt
 Gene Zharov
 geo909
 GeorgeHJ
+ggbari
 Gomme Bidule
 granderil
 greye
@@ -103,8 +115,10 @@ Hans Kristian Otnes Berge
 Hans Petter Jansson (hpjansson)
 heelsleeh
 Hendrik Jaeger (henk)
+Henrik Holst (hholst80)
 hofheinz
 hutou
+infinitewhileloop
 Ink (inknoir)
 iSeeU816
 IvanBarsukov
@@ -112,7 +126,7 @@ j-xella
 J. Reis
 Jakob Helmecke
 Janek (xeruf)
-Jason Dreisbach (a.k.a. jtdreisb)
+Jason Dreisbach (jtdreisb)
 Jason W. Ryan
 jcarreja
 Jeet Sukumaran (jeetsukumaran)
@@ -162,10 +176,12 @@ Marcin Juszkiewicz (hrw)
 Marcin Kurczewski (rr-)
 Marcos Cruz
 Marius Schmidl
+Mark S. (Markuzcha)
+Markus Elfring (elfring)
 Martin Fischer
 Marton Balazs (balmar)
 mateusz28
-Matt Alexander (a.k.a. mattalexx)
+Matt Alexander (mattalexx)
 Matthias Braun (mb720)
 maxigaz
 Melandel
@@ -201,6 +217,7 @@ Olmo Kramer
 Ondrej Novy (onovy)
 oo-
 opennota
+Oskar Grunning (sQVe)
 ovk
 p-kolacz
 Pavel (neoascetic)
@@ -219,7 +236,6 @@ piotryordanov
 PRESFIL
 qinghao (haobug)
 qsmodo
-Safal Piya (mrsafalpiya)
 r0ck
 r44083
 rafasc
@@ -242,6 +258,7 @@ RR0925
 Ruslan Osmanov (rosmanov)
 Russell Urquhart
 rwtallant13
+Safal Piya (mrsafalpiya)
 SanLe
 santhoshr
 Sassan Haradji (sassanh)
@@ -251,6 +268,7 @@ SearyBlue
 Sebastian Cyprych
 Seok Won Lee (ijleesw)
 Sergei Shilovsky
+Sergei Trofimovich (trofi)
 Seth VanHeulen (svanheulen)
 Shakil Akhtar
 sharklasers996
@@ -263,13 +281,14 @@ Stas Panteleev
 Stephano (cao)
 Stephen Horst (sjhorst)
 Stephen L. Holtz (stephenholtz)
-Stéphane (istib)
 StillSteal
+Stéphane (istib)
 sudo-nice
 Svadkos
 Svenn Are Bjerkem (svenn)
 svenn71
-Svyatoslav Mishyn (juef)
+Sylwia Ptasinska (SylEleuth)
+Szilárd Andai
 tagwint
 Taras Halturin (halturin)
 tcftbl
@@ -289,6 +308,7 @@ tYGjQCsvVI
 Tykin
 Tyler Spivey
 Typo
+user451421541757324
 Valery Ushakov (nbuwe)
 verdult
 Vigi
@@ -296,8 +316,8 @@ Vlad Glagolev (Stealth)
 Von Welch
 vzel
 willemw12
-y2kbugger
 Xirui Zhao (xiruizhao)
+y2kbugger
 Yang Zou
 yanzhang0219
 Yuriy Artemyev (anuvyklack)
@@ -306,10 +326,4 @@ Zsolt Udvari (uzsolt)
 zsugabubus
 Евгений Жаров (ezharov)
 Сергей Соловьёв (Sergej Soloviov)
-
-Special thanks to Colin Cartade for a lot of useful ideas, alpha testing on
-Linux and his will to make vifm better.  Another big thanks to Alexandre Viau
-for alpha testing on Windows and useful suggestions.  Acknowledgments also to
-filterfalse for his valuable suggestions, plugin/documentation patches and
-constant feedback.  Another great user is Svyatoslav Mishyn (a.k.a. juef),
-who performs alpha testing and provides valuable feedback.
+高浩亮 (haolian9)
diff --git a/TODO b/TODO
index 8314db9..991bbab 100644
--- a/TODO
+++ b/TODO
@@ -11,7 +11,6 @@ Basic things:
   * Responsive TUI during blocking operations.
   * Better failed operation messages. (partially done)
   * Make status bar able to display error and info messages at the same time.
-  * Editing registers in Vim.
   * Command to create multiple copies of selected file.
 
 Documentation:
@@ -33,7 +32,6 @@ Vi(m) specific features:
   * nrformats option as in Vim.
   * Better ranges (add support for search patterns).
   * :map without arguments.
-  * Don't move cursor on search failure.
   * Consider adding support for sequences like \<cr>.
   * Completion of first argument for :map commands.
   * Make gU2U and similar commands work as in Vim.
@@ -110,7 +108,6 @@ Possible things to add:
 
 Questionable things:
   * Full client-server operation of vifm (separate core from TUI).
-  * %p/%I macro to prompt user for input.
   * Maybe re-position cursor after :!ls%u.
   * Remove items from :history command menu?
   * Maybe use @path syntax to load custom views.
diff --git a/aclocal.m4 b/aclocal.m4
index 12a4ea5..ca3330e 100644
--- a/aclocal.m4
+++ b/aclocal.m4
@@ -1165,3 +1165,4 @@ AC_SUBST([am__untar])
 ]) # _AM_PROG_TAR
 
 m4_include([build-aux/m4/ax_check_compile_flag.m4])
+m4_include([build-aux/m4/ax_pthread.m4])
diff --git a/build-aux/config.h.in b/build-aux/config.h.in
index ffce445..ecc29be 100644
--- a/build-aux/config.h.in
+++ b/build-aux/config.h.in
@@ -74,12 +74,21 @@
 /* Define to 1 if you have the <mntent.h> header file. */
 #undef HAVE_MNTENT_H
 
+/* Have PTHREAD_PRIO_INHERIT. */
+#undef HAVE_PTHREAD_PRIO_INHERIT
+
+/* Define to 1 if you have the `random' function. */
+#undef HAVE_RANDOM
+
 /* Define to 1 if you have the `reallocarray' function. */
 #undef HAVE_REALLOCARRAY
 
 /* set_escdelay() function is available. */
 #undef HAVE_SET_ESCDELAY_FUNC
 
+/* Define to 1 if you have the `srandom' function. */
+#undef HAVE_SRANDOM
+
 /* Define to 1 if you have the <stdint.h> header file. */
 #undef HAVE_STDINT_H
 
@@ -158,6 +167,10 @@
 /* Define to the version of this package. */
 #undef PACKAGE_VERSION
 
+/* Define to necessary symbol if this constant uses a non-standard name on
+   your system. */
+#undef PTHREAD_CREATE_JOINABLE
+
 /* Define to 1 if you have the ANSI C header files. */
 #undef STDC_HEADERS
 
diff --git a/build-aux/m4/ax_pthread.m4 b/build-aux/m4/ax_pthread.m4
new file mode 100644
index 0000000..9f35d13
--- /dev/null
+++ b/build-aux/m4/ax_pthread.m4
@@ -0,0 +1,522 @@
+# ===========================================================================
+#        https://www.gnu.org/software/autoconf-archive/ax_pthread.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_PTHREAD([ACTION-IF-FOUND[, ACTION-IF-NOT-FOUND]])
+#
+# DESCRIPTION
+#
+#   This macro figures out how to build C programs using POSIX threads. It
+#   sets the PTHREAD_LIBS output variable to the threads library and linker
+#   flags, and the PTHREAD_CFLAGS output variable to any special C compiler
+#   flags that are needed. (The user can also force certain compiler
+#   flags/libs to be tested by setting these environment variables.)
+#
+#   Also sets PTHREAD_CC and PTHREAD_CXX to any special C compiler that is
+#   needed for multi-threaded programs (defaults to the value of CC
+#   respectively CXX otherwise). (This is necessary on e.g. AIX to use the
+#   special cc_r/CC_r compiler alias.)
+#
+#   NOTE: You are assumed to not only compile your program with these flags,
+#   but also to link with them as well. For example, you might link with
+#   $PTHREAD_CC $CFLAGS $PTHREAD_CFLAGS $LDFLAGS ... $PTHREAD_LIBS $LIBS
+#   $PTHREAD_CXX $CXXFLAGS $PTHREAD_CFLAGS $LDFLAGS ... $PTHREAD_LIBS $LIBS
+#
+#   If you are only building threaded programs, you may wish to use these
+#   variables in your default LIBS, CFLAGS, and CC:
+#
+#     LIBS="$PTHREAD_LIBS $LIBS"
+#     CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+#     CXXFLAGS="$CXXFLAGS $PTHREAD_CFLAGS"
+#     CC="$PTHREAD_CC"
+#     CXX="$PTHREAD_CXX"
+#
+#   In addition, if the PTHREAD_CREATE_JOINABLE thread-attribute constant
+#   has a nonstandard name, this macro defines PTHREAD_CREATE_JOINABLE to
+#   that name (e.g. PTHREAD_CREATE_UNDETACHED on AIX).
+#
+#   Also HAVE_PTHREAD_PRIO_INHERIT is defined if pthread is found and the
+#   PTHREAD_PRIO_INHERIT symbol is defined when compiling with
+#   PTHREAD_CFLAGS.
+#
+#   ACTION-IF-FOUND is a list of shell commands to run if a threads library
+#   is found, and ACTION-IF-NOT-FOUND is a list of commands to run it if it
+#   is not found. If ACTION-IF-FOUND is not specified, the default action
+#   will define HAVE_PTHREAD.
+#
+#   Please let the authors know if this macro fails on any platform, or if
+#   you have any other suggestions or comments. This macro was based on work
+#   by SGJ on autoconf scripts for FFTW (http://www.fftw.org/) (with help
+#   from M. Frigo), as well as ac_pthread and hb_pthread macros posted by
+#   Alejandro Forero Cuervo to the autoconf macro repository. We are also
+#   grateful for the helpful feedback of numerous users.
+#
+#   Updated for Autoconf 2.68 by Daniel Richard G.
+#
+# LICENSE
+#
+#   Copyright (c) 2008 Steven G. Johnson <stevenj@alum.mit.edu>
+#   Copyright (c) 2011 Daniel Richard G. <skunk@iSKUNK.ORG>
+#   Copyright (c) 2019 Marc Stevens <marc.stevens@cwi.nl>
+#
+#   This program is free software: you can redistribute it and/or modify it
+#   under the terms of the GNU General Public License as published by the
+#   Free Software Foundation, either version 3 of the License, or (at your
+#   option) any later version.
+#
+#   This program is distributed in the hope that it will be useful, but
+#   WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+#   Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License along
+#   with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+#   As a special exception, the respective Autoconf Macro's copyright owner
+#   gives unlimited permission to copy, distribute and modify the configure
+#   scripts that are the output of Autoconf when processing the Macro. You
+#   need not follow the terms of the GNU General Public License when using
+#   or distributing such scripts, even though portions of the text of the
+#   Macro appear in them. The GNU General Public License (GPL) does govern
+#   all other use of the material that constitutes the Autoconf Macro.
+#
+#   This special exception to the GPL applies to versions of the Autoconf
+#   Macro released by the Autoconf Archive. When you make and distribute a
+#   modified version of the Autoconf Macro, you may extend this special
+#   exception to the GPL to apply to your modified version as well.
+
+#serial 31
+
+AU_ALIAS([ACX_PTHREAD], [AX_PTHREAD])
+AC_DEFUN([AX_PTHREAD], [
+AC_REQUIRE([AC_CANONICAL_HOST])
+AC_REQUIRE([AC_PROG_CC])
+AC_REQUIRE([AC_PROG_SED])
+AC_LANG_PUSH([C])
+ax_pthread_ok=no
+
+# We used to check for pthread.h first, but this fails if pthread.h
+# requires special compiler flags (e.g. on Tru64 or Sequent).
+# It gets checked for in the link test anyway.
+
+# First of all, check if the user has set any of the PTHREAD_LIBS,
+# etcetera environment variables, and if threads linking works using
+# them:
+if test "x$PTHREAD_CFLAGS$PTHREAD_LIBS" != "x"; then
+        ax_pthread_save_CC="$CC"
+        ax_pthread_save_CFLAGS="$CFLAGS"
+        ax_pthread_save_LIBS="$LIBS"
+        AS_IF([test "x$PTHREAD_CC" != "x"], [CC="$PTHREAD_CC"])
+        AS_IF([test "x$PTHREAD_CXX" != "x"], [CXX="$PTHREAD_CXX"])
+        CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+        LIBS="$PTHREAD_LIBS $LIBS"
+        AC_MSG_CHECKING([for pthread_join using $CC $PTHREAD_CFLAGS $PTHREAD_LIBS])
+        AC_LINK_IFELSE([AC_LANG_CALL([], [pthread_join])], [ax_pthread_ok=yes])
+        AC_MSG_RESULT([$ax_pthread_ok])
+        if test "x$ax_pthread_ok" = "xno"; then
+                PTHREAD_LIBS=""
+                PTHREAD_CFLAGS=""
+        fi
+        CC="$ax_pthread_save_CC"
+        CFLAGS="$ax_pthread_save_CFLAGS"
+        LIBS="$ax_pthread_save_LIBS"
+fi
+
+# We must check for the threads library under a number of different
+# names; the ordering is very important because some systems
+# (e.g. DEC) have both -lpthread and -lpthreads, where one of the
+# libraries is broken (non-POSIX).
+
+# Create a list of thread flags to try. Items with a "," contain both
+# C compiler flags (before ",") and linker flags (after ","). Other items
+# starting with a "-" are C compiler flags, and remaining items are
+# library names, except for "none" which indicates that we try without
+# any flags at all, and "pthread-config" which is a program returning
+# the flags for the Pth emulation library.
+
+ax_pthread_flags="pthreads none -Kthread -pthread -pthreads -mthreads pthread --thread-safe -mt pthread-config"
+
+# The ordering *is* (sometimes) important.  Some notes on the
+# individual items follow:
+
+# pthreads: AIX (must check this before -lpthread)
+# none: in case threads are in libc; should be tried before -Kthread and
+#       other compiler flags to prevent continual compiler warnings
+# -Kthread: Sequent (threads in libc, but -Kthread needed for pthread.h)
+# -pthread: Linux/gcc (kernel threads), BSD/gcc (userland threads), Tru64
+#           (Note: HP C rejects this with "bad form for `-t' option")
+# -pthreads: Solaris/gcc (Note: HP C also rejects)
+# -mt: Sun Workshop C (may only link SunOS threads [-lthread], but it
+#      doesn't hurt to check since this sometimes defines pthreads and
+#      -D_REENTRANT too), HP C (must be checked before -lpthread, which
+#      is present but should not be used directly; and before -mthreads,
+#      because the compiler interprets this as "-mt" + "-hreads")
+# -mthreads: Mingw32/gcc, Lynx/gcc
+# pthread: Linux, etcetera
+# --thread-safe: KAI C++
+# pthread-config: use pthread-config program (for GNU Pth library)
+
+case $host_os in
+
+        freebsd*)
+
+        # -kthread: FreeBSD kernel threads (preferred to -pthread since SMP-able)
+        # lthread: LinuxThreads port on FreeBSD (also preferred to -pthread)
+
+        ax_pthread_flags="-kthread lthread $ax_pthread_flags"
+        ;;
+
+        hpux*)
+
+        # From the cc(1) man page: "[-mt] Sets various -D flags to enable
+        # multi-threading and also sets -lpthread."
+
+        ax_pthread_flags="-mt -pthread pthread $ax_pthread_flags"
+        ;;
+
+        openedition*)
+
+        # IBM z/OS requires a feature-test macro to be defined in order to
+        # enable POSIX threads at all, so give the user a hint if this is
+        # not set. (We don't define these ourselves, as they can affect
+        # other portions of the system API in unpredictable ways.)
+
+        AC_EGREP_CPP([AX_PTHREAD_ZOS_MISSING],
+            [
+#            if !defined(_OPEN_THREADS) && !defined(_UNIX03_THREADS)
+             AX_PTHREAD_ZOS_MISSING
+#            endif
+            ],
+            [AC_MSG_WARN([IBM z/OS requires -D_OPEN_THREADS or -D_UNIX03_THREADS to enable pthreads support.])])
+        ;;
+
+        solaris*)
+
+        # On Solaris (at least, for some versions), libc contains stubbed
+        # (non-functional) versions of the pthreads routines, so link-based
+        # tests will erroneously succeed. (N.B.: The stubs are missing
+        # pthread_cleanup_push, or rather a function called by this macro,
+        # so we could check for that, but who knows whether they'll stub
+        # that too in a future libc.)  So we'll check first for the
+        # standard Solaris way of linking pthreads (-mt -lpthread).
+
+        ax_pthread_flags="-mt,-lpthread pthread $ax_pthread_flags"
+        ;;
+esac
+
+# Are we compiling with Clang?
+
+AC_CACHE_CHECK([whether $CC is Clang],
+    [ax_cv_PTHREAD_CLANG],
+    [ax_cv_PTHREAD_CLANG=no
+     # Note that Autoconf sets GCC=yes for Clang as well as GCC
+     if test "x$GCC" = "xyes"; then
+        AC_EGREP_CPP([AX_PTHREAD_CC_IS_CLANG],
+            [/* Note: Clang 2.7 lacks __clang_[a-z]+__ */
+#            if defined(__clang__) && defined(__llvm__)
+             AX_PTHREAD_CC_IS_CLANG
+#            endif
+            ],
+            [ax_cv_PTHREAD_CLANG=yes])
+     fi
+    ])
+ax_pthread_clang="$ax_cv_PTHREAD_CLANG"
+
+
+# GCC generally uses -pthread, or -pthreads on some platforms (e.g. SPARC)
+
+# Note that for GCC and Clang -pthread generally implies -lpthread,
+# except when -nostdlib is passed.
+# This is problematic using libtool to build C++ shared libraries with pthread:
+# [1] https://gcc.gnu.org/bugzilla/show_bug.cgi?id=25460
+# [2] https://bugzilla.redhat.com/show_bug.cgi?id=661333
+# [3] https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=468555
+# To solve this, first try -pthread together with -lpthread for GCC
+
+AS_IF([test "x$GCC" = "xyes"],
+      [ax_pthread_flags="-pthread,-lpthread -pthread -pthreads $ax_pthread_flags"])
+
+# Clang takes -pthread (never supported any other flag), but we'll try with -lpthread first
+
+AS_IF([test "x$ax_pthread_clang" = "xyes"],
+      [ax_pthread_flags="-pthread,-lpthread -pthread"])
+
+
+# The presence of a feature test macro requesting re-entrant function
+# definitions is, on some systems, a strong hint that pthreads support is
+# correctly enabled
+
+case $host_os in
+        darwin* | hpux* | linux* | osf* | solaris*)
+        ax_pthread_check_macro="_REENTRANT"
+        ;;
+
+        aix*)
+        ax_pthread_check_macro="_THREAD_SAFE"
+        ;;
+
+        *)
+        ax_pthread_check_macro="--"
+        ;;
+esac
+AS_IF([test "x$ax_pthread_check_macro" = "x--"],
+      [ax_pthread_check_cond=0],
+      [ax_pthread_check_cond="!defined($ax_pthread_check_macro)"])
+
+
+if test "x$ax_pthread_ok" = "xno"; then
+for ax_pthread_try_flag in $ax_pthread_flags; do
+
+        case $ax_pthread_try_flag in
+                none)
+                AC_MSG_CHECKING([whether pthreads work without any flags])
+                ;;
+
+                *,*)
+                PTHREAD_CFLAGS=`echo $ax_pthread_try_flag | sed "s/^\(.*\),\(.*\)$/\1/"`
+                PTHREAD_LIBS=`echo $ax_pthread_try_flag | sed "s/^\(.*\),\(.*\)$/\2/"`
+                AC_MSG_CHECKING([whether pthreads work with "$PTHREAD_CFLAGS" and "$PTHREAD_LIBS"])
+                ;;
+
+                -*)
+                AC_MSG_CHECKING([whether pthreads work with $ax_pthread_try_flag])
+                PTHREAD_CFLAGS="$ax_pthread_try_flag"
+                ;;
+
+                pthread-config)
+                AC_CHECK_PROG([ax_pthread_config], [pthread-config], [yes], [no])
+                AS_IF([test "x$ax_pthread_config" = "xno"], [continue])
+                PTHREAD_CFLAGS="`pthread-config --cflags`"
+                PTHREAD_LIBS="`pthread-config --ldflags` `pthread-config --libs`"
+                ;;
+
+                *)
+                AC_MSG_CHECKING([for the pthreads library -l$ax_pthread_try_flag])
+                PTHREAD_LIBS="-l$ax_pthread_try_flag"
+                ;;
+        esac
+
+        ax_pthread_save_CFLAGS="$CFLAGS"
+        ax_pthread_save_LIBS="$LIBS"
+        CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+        LIBS="$PTHREAD_LIBS $LIBS"
+
+        # Check for various functions.  We must include pthread.h,
+        # since some functions may be macros.  (On the Sequent, we
+        # need a special flag -Kthread to make this header compile.)
+        # We check for pthread_join because it is in -lpthread on IRIX
+        # while pthread_create is in libc.  We check for pthread_attr_init
+        # due to DEC craziness with -lpthreads.  We check for
+        # pthread_cleanup_push because it is one of the few pthread
+        # functions on Solaris that doesn't have a non-functional libc stub.
+        # We try pthread_create on general principles.
+
+        AC_LINK_IFELSE([AC_LANG_PROGRAM([#include <pthread.h>
+#                       if $ax_pthread_check_cond
+#                        error "$ax_pthread_check_macro must be defined"
+#                       endif
+                        static void *some_global = NULL;
+                        static void routine(void *a)
+                          {
+                             /* To avoid any unused-parameter or
+                                unused-but-set-parameter warning.  */
+                             some_global = a;
+                          }
+                        static void *start_routine(void *a) { return a; }],
+                       [pthread_t th; pthread_attr_t attr;
+                        pthread_create(&th, 0, start_routine, 0);
+                        pthread_join(th, 0);
+                        pthread_attr_init(&attr);
+                        pthread_cleanup_push(routine, 0);
+                        pthread_cleanup_pop(0) /* ; */])],
+            [ax_pthread_ok=yes],
+            [])
+
+        CFLAGS="$ax_pthread_save_CFLAGS"
+        LIBS="$ax_pthread_save_LIBS"
+
+        AC_MSG_RESULT([$ax_pthread_ok])
+        AS_IF([test "x$ax_pthread_ok" = "xyes"], [break])
+
+        PTHREAD_LIBS=""
+        PTHREAD_CFLAGS=""
+done
+fi
+
+
+# Clang needs special handling, because older versions handle the -pthread
+# option in a rather... idiosyncratic way
+
+if test "x$ax_pthread_clang" = "xyes"; then
+
+        # Clang takes -pthread; it has never supported any other flag
+
+        # (Note 1: This will need to be revisited if a system that Clang
+        # supports has POSIX threads in a separate library.  This tends not
+        # to be the way of modern systems, but it's conceivable.)
+
+        # (Note 2: On some systems, notably Darwin, -pthread is not needed
+        # to get POSIX threads support; the API is always present and
+        # active.  We could reasonably leave PTHREAD_CFLAGS empty.  But
+        # -pthread does define _REENTRANT, and while the Darwin headers
+        # ignore this macro, third-party headers might not.)
+
+        # However, older versions of Clang make a point of warning the user
+        # that, in an invocation where only linking and no compilation is
+        # taking place, the -pthread option has no effect ("argument unused
+        # during compilation").  They expect -pthread to be passed in only
+        # when source code is being compiled.
+        #
+        # Problem is, this is at odds with the way Automake and most other
+        # C build frameworks function, which is that the same flags used in
+        # compilation (CFLAGS) are also used in linking.  Many systems
+        # supported by AX_PTHREAD require exactly this for POSIX threads
+        # support, and in fact it is often not straightforward to specify a
+        # flag that is used only in the compilation phase and not in
+        # linking.  Such a scenario is extremely rare in practice.
+        #
+        # Even though use of the -pthread flag in linking would only print
+        # a warning, this can be a nuisance for well-run software projects
+        # that build with -Werror.  So if the active version of Clang has
+        # this misfeature, we search for an option to squash it.
+
+        AC_CACHE_CHECK([whether Clang needs flag to prevent "argument unused" warning when linking with -pthread],
+            [ax_cv_PTHREAD_CLANG_NO_WARN_FLAG],
+            [ax_cv_PTHREAD_CLANG_NO_WARN_FLAG=unknown
+             # Create an alternate version of $ac_link that compiles and
+             # links in two steps (.c -> .o, .o -> exe) instead of one
+             # (.c -> exe), because the warning occurs only in the second
+             # step
+             ax_pthread_save_ac_link="$ac_link"
+             ax_pthread_sed='s/conftest\.\$ac_ext/conftest.$ac_objext/g'
+             ax_pthread_link_step=`AS_ECHO(["$ac_link"]) | sed "$ax_pthread_sed"`
+             ax_pthread_2step_ac_link="($ac_compile) && (echo ==== >&5) && ($ax_pthread_link_step)"
+             ax_pthread_save_CFLAGS="$CFLAGS"
+             for ax_pthread_try in '' -Qunused-arguments -Wno-unused-command-line-argument unknown; do
+                AS_IF([test "x$ax_pthread_try" = "xunknown"], [break])
+                CFLAGS="-Werror -Wunknown-warning-option $ax_pthread_try -pthread $ax_pthread_save_CFLAGS"
+                ac_link="$ax_pthread_save_ac_link"
+                AC_LINK_IFELSE([AC_LANG_SOURCE([[int main(void){return 0;}]])],
+                    [ac_link="$ax_pthread_2step_ac_link"
+                     AC_LINK_IFELSE([AC_LANG_SOURCE([[int main(void){return 0;}]])],
+                         [break])
+                    ])
+             done
+             ac_link="$ax_pthread_save_ac_link"
+             CFLAGS="$ax_pthread_save_CFLAGS"
+             AS_IF([test "x$ax_pthread_try" = "x"], [ax_pthread_try=no])
+             ax_cv_PTHREAD_CLANG_NO_WARN_FLAG="$ax_pthread_try"
+            ])
+
+        case "$ax_cv_PTHREAD_CLANG_NO_WARN_FLAG" in
+                no | unknown) ;;
+                *) PTHREAD_CFLAGS="$ax_cv_PTHREAD_CLANG_NO_WARN_FLAG $PTHREAD_CFLAGS" ;;
+        esac
+
+fi # $ax_pthread_clang = yes
+
+
+
+# Various other checks:
+if test "x$ax_pthread_ok" = "xyes"; then
+        ax_pthread_save_CFLAGS="$CFLAGS"
+        ax_pthread_save_LIBS="$LIBS"
+        CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+        LIBS="$PTHREAD_LIBS $LIBS"
+
+        # Detect AIX lossage: JOINABLE attribute is called UNDETACHED.
+        AC_CACHE_CHECK([for joinable pthread attribute],
+            [ax_cv_PTHREAD_JOINABLE_ATTR],
+            [ax_cv_PTHREAD_JOINABLE_ATTR=unknown
+             for ax_pthread_attr in PTHREAD_CREATE_JOINABLE PTHREAD_CREATE_UNDETACHED; do
+                 AC_LINK_IFELSE([AC_LANG_PROGRAM([#include <pthread.h>],
+                                                 [int attr = $ax_pthread_attr; return attr /* ; */])],
+                                [ax_cv_PTHREAD_JOINABLE_ATTR=$ax_pthread_attr; break],
+                                [])
+             done
+            ])
+        AS_IF([test "x$ax_cv_PTHREAD_JOINABLE_ATTR" != "xunknown" && \
+               test "x$ax_cv_PTHREAD_JOINABLE_ATTR" != "xPTHREAD_CREATE_JOINABLE" && \
+               test "x$ax_pthread_joinable_attr_defined" != "xyes"],
+              [AC_DEFINE_UNQUOTED([PTHREAD_CREATE_JOINABLE],
+                                  [$ax_cv_PTHREAD_JOINABLE_ATTR],
+                                  [Define to necessary symbol if this constant
+                                   uses a non-standard name on your system.])
+               ax_pthread_joinable_attr_defined=yes
+              ])
+
+        AC_CACHE_CHECK([whether more special flags are required for pthreads],
+            [ax_cv_PTHREAD_SPECIAL_FLAGS],
+            [ax_cv_PTHREAD_SPECIAL_FLAGS=no
+             case $host_os in
+             solaris*)
+             ax_cv_PTHREAD_SPECIAL_FLAGS="-D_POSIX_PTHREAD_SEMANTICS"
+             ;;
+             esac
+            ])
+        AS_IF([test "x$ax_cv_PTHREAD_SPECIAL_FLAGS" != "xno" && \
+               test "x$ax_pthread_special_flags_added" != "xyes"],
+              [PTHREAD_CFLAGS="$ax_cv_PTHREAD_SPECIAL_FLAGS $PTHREAD_CFLAGS"
+               ax_pthread_special_flags_added=yes])
+
+        AC_CACHE_CHECK([for PTHREAD_PRIO_INHERIT],
+            [ax_cv_PTHREAD_PRIO_INHERIT],
+            [AC_LINK_IFELSE([AC_LANG_PROGRAM([[#include <pthread.h>]],
+                                             [[int i = PTHREAD_PRIO_INHERIT;
+                                               return i;]])],
+                            [ax_cv_PTHREAD_PRIO_INHERIT=yes],
+                            [ax_cv_PTHREAD_PRIO_INHERIT=no])
+            ])
+        AS_IF([test "x$ax_cv_PTHREAD_PRIO_INHERIT" = "xyes" && \
+               test "x$ax_pthread_prio_inherit_defined" != "xyes"],
+              [AC_DEFINE([HAVE_PTHREAD_PRIO_INHERIT], [1], [Have PTHREAD_PRIO_INHERIT.])
+               ax_pthread_prio_inherit_defined=yes
+              ])
+
+        CFLAGS="$ax_pthread_save_CFLAGS"
+        LIBS="$ax_pthread_save_LIBS"
+
+        # More AIX lossage: compile with *_r variant
+        if test "x$GCC" != "xyes"; then
+            case $host_os in
+                aix*)
+                AS_CASE(["x/$CC"],
+                    [x*/c89|x*/c89_128|x*/c99|x*/c99_128|x*/cc|x*/cc128|x*/xlc|x*/xlc_v6|x*/xlc128|x*/xlc128_v6],
+                    [#handle absolute path differently from PATH based program lookup
+                     AS_CASE(["x$CC"],
+                         [x/*],
+                         [
+			   AS_IF([AS_EXECUTABLE_P([${CC}_r])],[PTHREAD_CC="${CC}_r"])
+			   AS_IF([test "x${CXX}" != "x"], [AS_IF([AS_EXECUTABLE_P([${CXX}_r])],[PTHREAD_CXX="${CXX}_r"])])
+			 ],
+                         [
+			   AC_CHECK_PROGS([PTHREAD_CC],[${CC}_r],[$CC])
+			   AS_IF([test "x${CXX}" != "x"], [AC_CHECK_PROGS([PTHREAD_CXX],[${CXX}_r],[$CXX])])
+			 ]
+                     )
+                    ])
+                ;;
+            esac
+        fi
+fi
+
+test -n "$PTHREAD_CC" || PTHREAD_CC="$CC"
+test -n "$PTHREAD_CXX" || PTHREAD_CXX="$CXX"
+
+AC_SUBST([PTHREAD_LIBS])
+AC_SUBST([PTHREAD_CFLAGS])
+AC_SUBST([PTHREAD_CC])
+AC_SUBST([PTHREAD_CXX])
+
+# Finally, execute ACTION-IF-FOUND/ACTION-IF-NOT-FOUND:
+if test "x$ax_pthread_ok" = "xyes"; then
+        ifelse([$1],,[AC_DEFINE([HAVE_PTHREAD],[1],[Define if you have POSIX threads libraries and header files.])],[$1])
+        :
+else
+        ax_pthread_ok=no
+        $2
+fi
+AC_LANG_POP
+])dnl AX_PTHREAD
diff --git a/configure b/configure
index 777792e..0eb7204 100755
--- a/configure
+++ b/configure
@@ -1,6 +1,6 @@
 #! /bin/sh
 # Guess values for system-dependent variables and create Makefiles.
-# Generated by GNU Autoconf 2.69 for vifm 0.12.1.
+# Generated by GNU Autoconf 2.69 for vifm 0.13.
 #
 # Report bugs to <xaizek@posteo.net>.
 #
@@ -580,8 +580,8 @@ MAKEFLAGS=
 # Identity of this package.
 PACKAGE_NAME='vifm'
 PACKAGE_TARNAME='vifm'
-PACKAGE_VERSION='0.12.1'
-PACKAGE_STRING='vifm 0.12.1'
+PACKAGE_VERSION='0.13'
+PACKAGE_STRING='vifm 0.13'
 PACKAGE_BUGREPORT='xaizek@posteo.net'
 PACKAGE_URL='https://vifm.info'
 
@@ -627,12 +627,18 @@ am__EXEEXT_TRUE
 LTLIBOBJS
 LIBOBJS
 SANITIZERS_CFLAGS
+PTHREAD_CFLAGS
+PTHREAD_LIBS
+PTHREAD_CXX
+PTHREAD_CC
+ax_pthread_config
+SED
 DATA_SUFFIX
 IN_GIT_REPO
 GIT_PROG
 VIM_PROG
-SED_PROG
 PERL_PROG
+SED_PROG
 AWK_PROG
 COL_PROG
 MANGEN_PROG
@@ -1301,7 +1307,7 @@ if test "$ac_init_help" = "long"; then
   # Omit some internal or obsolete options to make the list less imposing.
   # This message is too long to be a string in the A/UX 3.1 sh.
   cat <<_ACEOF
-\`configure' configures vifm 0.12.1 to adapt to many kinds of systems.
+\`configure' configures vifm 0.13 to adapt to many kinds of systems.
 
 Usage: $0 [OPTION]... [VAR=VALUE]...
 
@@ -1371,7 +1377,7 @@ fi
 
 if test -n "$ac_init_help"; then
   case $ac_init_help in
-     short | recursive ) echo "Configuration of vifm 0.12.1:";;
+     short | recursive ) echo "Configuration of vifm 0.13:";;
    esac
   cat <<\_ACEOF
 
@@ -1495,7 +1501,7 @@ fi
 test -n "$ac_init_help" && exit $ac_status
 if $ac_init_version; then
   cat <<\_ACEOF
-vifm configure 0.12.1
+vifm configure 0.13
 generated by GNU Autoconf 2.69
 
 Copyright (C) 2012 Free Software Foundation, Inc.
@@ -2021,7 +2027,7 @@ cat >config.log <<_ACEOF
 This file contains any messages produced by compilers while
 running configure, to aid debugging if configure makes a mistake.
 
-It was created by vifm $as_me 0.12.1, which was
+It was created by vifm $as_me 0.13, which was
 generated by GNU Autoconf 2.69.  Invocation command line was
 
   $ $0 $@
@@ -2886,7 +2892,7 @@ fi
 
 # Define the identity of the package.
  PACKAGE='vifm'
- VERSION='0.12.1'
+ VERSION='0.13'
 
 
 cat >>confdefs.h <<_ACEOF
@@ -6690,15 +6696,6 @@ else
   as_fn_error $? "dup2() function not found." "$LINENO" 5
 fi
 
-if test -n "$HAVE_MNTENT_H" ; then
-    ac_fn_c_check_func "$LINENO" "endmntent" "ac_cv_func_endmntent"
-if test "x$ac_cv_func_endmntent" = xyes; then :
-
-else
-  as_fn_error $? "endmntent() function not found." "$LINENO" 5
-fi
-
-fi
 ac_fn_c_check_func "$LINENO" "execve" "ac_cv_func_execve"
 if test "x$ac_cv_func_execve" = xyes; then :
 
@@ -6797,17 +6794,6 @@ else
   as_fn_error $? "free() function not found." "$LINENO" 5
 fi
 
-for ac_func in futimens
-do :
-  ac_fn_c_check_func "$LINENO" "futimens" "ac_cv_func_futimens"
-if test "x$ac_cv_func_futimens" = xyes; then :
-  cat >>confdefs.h <<_ACEOF
-#define HAVE_FUTIMENS 1
-_ACEOF
-
-fi
-done
-
 ac_fn_c_check_func "$LINENO" "fwrite" "ac_cv_func_fwrite"
 if test "x$ac_cv_func_fwrite" = xyes; then :
 
@@ -6864,15 +6850,6 @@ else
   as_fn_error $? "getgrnam() function not found." "$LINENO" 5
 fi
 
-if test -n "$HAVE_MNTENT_H" ; then
-    ac_fn_c_check_func "$LINENO" "getmntent" "ac_cv_func_getmntent"
-if test "x$ac_cv_func_getmntent" = xyes; then :
-
-else
-  as_fn_error $? "getmntent() function not found." "$LINENO" 5
-fi
-
-fi
 ac_fn_c_check_func "$LINENO" "getpid" "ac_cv_func_getpid"
 if test "x$ac_cv_func_getpid" = xyes; then :
 
@@ -7027,6 +7004,13 @@ else
   as_fn_error $? "mkdir() function not found." "$LINENO" 5
 fi
 
+ac_fn_c_check_func "$LINENO" "mkstemp" "ac_cv_func_mkstemp"
+if test "x$ac_cv_func_mkstemp" = xyes; then :
+
+else
+  as_fn_error $? "mkstemp() function not found." "$LINENO" 5
+fi
+
 ac_fn_c_check_func "$LINENO" "opendir" "ac_cv_func_opendir"
 if test "x$ac_cv_func_opendir" = xyes; then :
 
@@ -7097,6 +7081,13 @@ else
   as_fn_error $? "qsort() function not found." "$LINENO" 5
 fi
 
+ac_fn_c_check_func "$LINENO" "rand" "ac_cv_func_rand"
+if test "x$ac_cv_func_rand" = xyes; then :
+
+else
+  as_fn_error $? "rand() function not found." "$LINENO" 5
+fi
+
 ac_fn_c_check_func "$LINENO" "read" "ac_cv_func_read"
 if test "x$ac_cv_func_read" = xyes; then :
 
@@ -7118,17 +7109,6 @@ else
   as_fn_error $? "realloc() function not found." "$LINENO" 5
 fi
 
-for ac_func in reallocarray
-do :
-  ac_fn_c_check_func "$LINENO" "reallocarray" "ac_cv_func_reallocarray"
-if test "x$ac_cv_func_reallocarray" = xyes; then :
-  cat >>confdefs.h <<_ACEOF
-#define HAVE_REALLOCARRAY 1
-_ACEOF
-
-fi
-done
-
 ac_fn_c_check_func "$LINENO" "realpath" "ac_cv_func_realpath"
 if test "x$ac_cv_func_realpath" = xyes; then :
 
@@ -7178,15 +7158,6 @@ else
   as_fn_error $? "setlocale() function not found." "$LINENO" 5
 fi
 
-if test -n "$HAVE_MNTENT_H" ; then
-    ac_fn_c_check_func "$LINENO" "setmntent" "ac_cv_func_setmntent"
-if test "x$ac_cv_func_setmntent" = xyes; then :
-
-else
-  as_fn_error $? "setmntent() function not found." "$LINENO" 5
-fi
-
-fi
 ac_fn_c_check_func "$LINENO" "setpgid" "ac_cv_func_setpgid"
 if test "x$ac_cv_func_setpgid" = xyes; then :
 
@@ -7257,6 +7228,13 @@ else
   as_fn_error $? "sprintf() function not found." "$LINENO" 5
 fi
 
+ac_fn_c_check_func "$LINENO" "srand" "ac_cv_func_srand"
+if test "x$ac_cv_func_srand" = xyes; then :
+
+else
+  as_fn_error $? "srand() function not found." "$LINENO" 5
+fi
+
 ac_fn_c_check_func "$LINENO" "strcasecmp" "ac_cv_func_strcasecmp"
 if test "x$ac_cv_func_strcasecmp" = xyes; then :
 
@@ -7580,6 +7558,65 @@ else
 fi
 
 
+for ac_func in futimens
+do :
+  ac_fn_c_check_func "$LINENO" "futimens" "ac_cv_func_futimens"
+if test "x$ac_cv_func_futimens" = xyes; then :
+  cat >>confdefs.h <<_ACEOF
+#define HAVE_FUTIMENS 1
+_ACEOF
+
+fi
+done
+
+for ac_func in random srandom
+do :
+  as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
+ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
+if eval test \"x\$"$as_ac_var"\" = x"yes"; then :
+  cat >>confdefs.h <<_ACEOF
+#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
+_ACEOF
+
+fi
+done
+
+for ac_func in reallocarray
+do :
+  ac_fn_c_check_func "$LINENO" "reallocarray" "ac_cv_func_reallocarray"
+if test "x$ac_cv_func_reallocarray" = xyes; then :
+  cat >>confdefs.h <<_ACEOF
+#define HAVE_REALLOCARRAY 1
+_ACEOF
+
+fi
+done
+
+
+if test -n "$HAVE_MNTENT_H" ; then
+    ac_fn_c_check_func "$LINENO" "endmntent" "ac_cv_func_endmntent"
+if test "x$ac_cv_func_endmntent" = xyes; then :
+
+else
+  as_fn_error $? "endmntent() function not found." "$LINENO" 5
+fi
+
+    ac_fn_c_check_func "$LINENO" "getmntent" "ac_cv_func_getmntent"
+if test "x$ac_cv_func_getmntent" = xyes; then :
+
+else
+  as_fn_error $? "getmntent() function not found." "$LINENO" 5
+fi
+
+    ac_fn_c_check_func "$LINENO" "setmntent" "ac_cv_func_setmntent"
+if test "x$ac_cv_func_setmntent" = xyes; then :
+
+else
+  as_fn_error $? "setmntent() function not found." "$LINENO" 5
+fi
+
+fi
+
 ac_fn_c_check_decl "$LINENO" "_PC_CASE_SENSITIVE" "ac_cv_have_decl__PC_CASE_SENSITIVE" "#include <unistd.h>
 "
 if test "x$ac_cv_have_decl__PC_CASE_SENSITIVE" = xyes; then :
@@ -8156,15 +8193,15 @@ fi
 
 
 
-# Extract the first word of "perl", so it can be a program name with args.
-set dummy perl; ac_word=$2
+# Extract the first word of "sed", so it can be a program name with args.
+set dummy sed; ac_word=$2
 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
 $as_echo_n "checking for $ac_word... " >&6; }
-if ${ac_cv_prog_PERL_PROG+:} false; then :
+if ${ac_cv_prog_SED_PROG+:} false; then :
   $as_echo_n "(cached) " >&6
 else
-  if test -n "$PERL_PROG"; then
-  ac_cv_prog_PERL_PROG="$PERL_PROG" # Let the user override the test.
+  if test -n "$SED_PROG"; then
+  ac_cv_prog_SED_PROG="$SED_PROG" # Let the user override the test.
 else
 as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
 for as_dir in $PATH
@@ -8173,7 +8210,7 @@ do
   test -z "$as_dir" && as_dir=.
     for ac_exec_ext in '' $ac_executable_extensions; do
   if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then
-    ac_cv_prog_PERL_PROG="perl"
+    ac_cv_prog_SED_PROG="sed"
     $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5
     break 2
   fi
@@ -8183,10 +8220,10 @@ IFS=$as_save_IFS
 
 fi
 fi
-PERL_PROG=$ac_cv_prog_PERL_PROG
-if test -n "$PERL_PROG"; then
-  { $as_echo "$as_me:${as_lineno-$LINENO}: result: $PERL_PROG" >&5
-$as_echo "$PERL_PROG" >&6; }
+SED_PROG=$ac_cv_prog_SED_PROG
+if test -n "$SED_PROG"; then
+  { $as_echo "$as_me:${as_lineno-$LINENO}: result: $SED_PROG" >&5
+$as_echo "$SED_PROG" >&6; }
 else
   { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
 $as_echo "no" >&6; }
@@ -8194,15 +8231,15 @@ fi
 
 
 
-# Extract the first word of "sed", so it can be a program name with args.
-set dummy sed; ac_word=$2
+# Extract the first word of "perl", so it can be a program name with args.
+set dummy perl; ac_word=$2
 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
 $as_echo_n "checking for $ac_word... " >&6; }
-if ${ac_cv_prog_SED_PROG+:} false; then :
+if ${ac_cv_prog_PERL_PROG+:} false; then :
   $as_echo_n "(cached) " >&6
 else
-  if test -n "$SED_PROG"; then
-  ac_cv_prog_SED_PROG="$SED_PROG" # Let the user override the test.
+  if test -n "$PERL_PROG"; then
+  ac_cv_prog_PERL_PROG="$PERL_PROG" # Let the user override the test.
 else
 as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
 for as_dir in $PATH
@@ -8211,7 +8248,7 @@ do
   test -z "$as_dir" && as_dir=.
     for ac_exec_ext in '' $ac_executable_extensions; do
   if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then
-    ac_cv_prog_SED_PROG="sed"
+    ac_cv_prog_PERL_PROG="perl"
     $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5
     break 2
   fi
@@ -8221,10 +8258,10 @@ IFS=$as_save_IFS
 
 fi
 fi
-SED_PROG=$ac_cv_prog_SED_PROG
-if test -n "$SED_PROG"; then
-  { $as_echo "$as_me:${as_lineno-$LINENO}: result: $SED_PROG" >&5
-$as_echo "$SED_PROG" >&6; }
+PERL_PROG=$ac_cv_prog_PERL_PROG
+if test -n "$PERL_PROG"; then
+  { $as_echo "$as_me:${as_lineno-$LINENO}: result: $PERL_PROG" >&5
+$as_echo "$PERL_PROG" >&6; }
 else
   { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
 $as_echo "no" >&6; }
@@ -8270,6 +8307,10 @@ fi
 
 
 
+if test "x${PERL_PROG}${VIM_PROG}" == 'x'; then
+    as_fn_error $? "Either perl or Vim is necessary to generate tags for documentation in Vim's format." "$LINENO" 5
+fi
+
 # Extract the first word of "git", so it can be a program name with args.
 set dummy git; ac_word=$2
 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
@@ -8511,96 +8552,804 @@ if test "x$ac_cv_lib_rt_shm_open" = xyes; then :
 fi
 
 
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether C compiler accepts -pthread" >&5
-$as_echo_n "checking whether C compiler accepts -pthread... " >&6; }
-if ${ax_cv_check_cflags___pthread+:} false; then :
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for a sed that does not truncate output" >&5
+$as_echo_n "checking for a sed that does not truncate output... " >&6; }
+if ${ac_cv_path_SED+:} false; then :
   $as_echo_n "(cached) " >&6
 else
+            ac_script=s/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/
+     for ac_i in 1 2 3 4 5 6 7; do
+       ac_script="$ac_script$as_nl$ac_script"
+     done
+     echo "$ac_script" 2>/dev/null | sed 99q >conftest.sed
+     { ac_script=; unset ac_script;}
+     if test -z "$SED"; then
+  ac_path_SED_found=false
+  # Loop through the user's path and test for each of PROGNAME-LIST
+  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  test -z "$as_dir" && as_dir=.
+    for ac_prog in sed gsed; do
+    for ac_exec_ext in '' $ac_executable_extensions; do
+      ac_path_SED="$as_dir/$ac_prog$ac_exec_ext"
+      as_fn_executable_p "$ac_path_SED" || continue
+# Check for GNU ac_path_SED and select it if it is found.
+  # Check for GNU $ac_path_SED
+case `"$ac_path_SED" --version 2>&1` in
+*GNU*)
+  ac_cv_path_SED="$ac_path_SED" ac_path_SED_found=:;;
+*)
+  ac_count=0
+  $as_echo_n 0123456789 >"conftest.in"
+  while :
+  do
+    cat "conftest.in" "conftest.in" >"conftest.tmp"
+    mv "conftest.tmp" "conftest.in"
+    cp "conftest.in" "conftest.nl"
+    $as_echo '' >> "conftest.nl"
+    "$ac_path_SED" -f conftest.sed < "conftest.nl" >"conftest.out" 2>/dev/null || break
+    diff "conftest.out" "conftest.nl" >/dev/null 2>&1 || break
+    as_fn_arith $ac_count + 1 && ac_count=$as_val
+    if test $ac_count -gt ${ac_path_SED_max-0}; then
+      # Best one so far, save it but keep looking for a better one
+      ac_cv_path_SED="$ac_path_SED"
+      ac_path_SED_max=$ac_count
+    fi
+    # 10*(2^10) chars as input seems more than enough
+    test $ac_count -gt 10 && break
+  done
+  rm -f conftest.in conftest.tmp conftest.nl conftest.out;;
+esac
 
-  ax_check_save_flags=$CFLAGS
-  CFLAGS="$CFLAGS  -pthread"
-  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+      $ac_path_SED_found && break 3
+    done
+  done
+  done
+IFS=$as_save_IFS
+  if test -z "$ac_cv_path_SED"; then
+    as_fn_error $? "no acceptable sed could be found in \$PATH" "$LINENO" 5
+  fi
+else
+  ac_cv_path_SED=$SED
+fi
+
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_SED" >&5
+$as_echo "$ac_cv_path_SED" >&6; }
+ SED="$ac_cv_path_SED"
+  rm -f conftest.sed
+
+
+
+
+
+ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+ax_pthread_ok=no
+
+# We used to check for pthread.h first, but this fails if pthread.h
+# requires special compiler flags (e.g. on Tru64 or Sequent).
+# It gets checked for in the link test anyway.
+
+# First of all, check if the user has set any of the PTHREAD_LIBS,
+# etcetera environment variables, and if threads linking works using
+# them:
+if test "x$PTHREAD_CFLAGS$PTHREAD_LIBS" != "x"; then
+        ax_pthread_save_CC="$CC"
+        ax_pthread_save_CFLAGS="$CFLAGS"
+        ax_pthread_save_LIBS="$LIBS"
+        if test "x$PTHREAD_CC" != "x"; then :
+  CC="$PTHREAD_CC"
+fi
+        if test "x$PTHREAD_CXX" != "x"; then :
+  CXX="$PTHREAD_CXX"
+fi
+        CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+        LIBS="$PTHREAD_LIBS $LIBS"
+        { $as_echo "$as_me:${as_lineno-$LINENO}: checking for pthread_join using $CC $PTHREAD_CFLAGS $PTHREAD_LIBS" >&5
+$as_echo_n "checking for pthread_join using $CC $PTHREAD_CFLAGS $PTHREAD_LIBS... " >&6; }
+        cat confdefs.h - <<_ACEOF >conftest.$ac_ext
 /* end confdefs.h.  */
 
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char pthread_join ();
 int
 main ()
 {
-
+return pthread_join ();
   ;
   return 0;
 }
 _ACEOF
-if ac_fn_c_try_compile "$LINENO"; then :
-  ax_cv_check_cflags___pthread=yes
+if ac_fn_c_try_link "$LINENO"; then :
+  ax_pthread_ok=yes
+fi
+rm -f core conftest.err conftest.$ac_objext \
+    conftest$ac_exeext conftest.$ac_ext
+        { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ax_pthread_ok" >&5
+$as_echo "$ax_pthread_ok" >&6; }
+        if test "x$ax_pthread_ok" = "xno"; then
+                PTHREAD_LIBS=""
+                PTHREAD_CFLAGS=""
+        fi
+        CC="$ax_pthread_save_CC"
+        CFLAGS="$ax_pthread_save_CFLAGS"
+        LIBS="$ax_pthread_save_LIBS"
+fi
+
+# We must check for the threads library under a number of different
+# names; the ordering is very important because some systems
+# (e.g. DEC) have both -lpthread and -lpthreads, where one of the
+# libraries is broken (non-POSIX).
+
+# Create a list of thread flags to try. Items with a "," contain both
+# C compiler flags (before ",") and linker flags (after ","). Other items
+# starting with a "-" are C compiler flags, and remaining items are
+# library names, except for "none" which indicates that we try without
+# any flags at all, and "pthread-config" which is a program returning
+# the flags for the Pth emulation library.
+
+ax_pthread_flags="pthreads none -Kthread -pthread -pthreads -mthreads pthread --thread-safe -mt pthread-config"
+
+# The ordering *is* (sometimes) important.  Some notes on the
+# individual items follow:
+
+# pthreads: AIX (must check this before -lpthread)
+# none: in case threads are in libc; should be tried before -Kthread and
+#       other compiler flags to prevent continual compiler warnings
+# -Kthread: Sequent (threads in libc, but -Kthread needed for pthread.h)
+# -pthread: Linux/gcc (kernel threads), BSD/gcc (userland threads), Tru64
+#           (Note: HP C rejects this with "bad form for `-t' option")
+# -pthreads: Solaris/gcc (Note: HP C also rejects)
+# -mt: Sun Workshop C (may only link SunOS threads [-lthread], but it
+#      doesn't hurt to check since this sometimes defines pthreads and
+#      -D_REENTRANT too), HP C (must be checked before -lpthread, which
+#      is present but should not be used directly; and before -mthreads,
+#      because the compiler interprets this as "-mt" + "-hreads")
+# -mthreads: Mingw32/gcc, Lynx/gcc
+# pthread: Linux, etcetera
+# --thread-safe: KAI C++
+# pthread-config: use pthread-config program (for GNU Pth library)
+
+case $host_os in
+
+        freebsd*)
+
+        # -kthread: FreeBSD kernel threads (preferred to -pthread since SMP-able)
+        # lthread: LinuxThreads port on FreeBSD (also preferred to -pthread)
+
+        ax_pthread_flags="-kthread lthread $ax_pthread_flags"
+        ;;
+
+        hpux*)
+
+        # From the cc(1) man page: "[-mt] Sets various -D flags to enable
+        # multi-threading and also sets -lpthread."
+
+        ax_pthread_flags="-mt -pthread pthread $ax_pthread_flags"
+        ;;
+
+        openedition*)
+
+        # IBM z/OS requires a feature-test macro to be defined in order to
+        # enable POSIX threads at all, so give the user a hint if this is
+        # not set. (We don't define these ourselves, as they can affect
+        # other portions of the system API in unpredictable ways.)
+
+        cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+#            if !defined(_OPEN_THREADS) && !defined(_UNIX03_THREADS)
+             AX_PTHREAD_ZOS_MISSING
+#            endif
+
+_ACEOF
+if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
+  $EGREP "AX_PTHREAD_ZOS_MISSING" >/dev/null 2>&1; then :
+  { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: IBM z/OS requires -D_OPEN_THREADS or -D_UNIX03_THREADS to enable pthreads support." >&5
+$as_echo "$as_me: WARNING: IBM z/OS requires -D_OPEN_THREADS or -D_UNIX03_THREADS to enable pthreads support." >&2;}
+fi
+rm -f conftest*
+
+        ;;
+
+        solaris*)
+
+        # On Solaris (at least, for some versions), libc contains stubbed
+        # (non-functional) versions of the pthreads routines, so link-based
+        # tests will erroneously succeed. (N.B.: The stubs are missing
+        # pthread_cleanup_push, or rather a function called by this macro,
+        # so we could check for that, but who knows whether they'll stub
+        # that too in a future libc.)  So we'll check first for the
+        # standard Solaris way of linking pthreads (-mt -lpthread).
+
+        ax_pthread_flags="-mt,-lpthread pthread $ax_pthread_flags"
+        ;;
+esac
+
+# Are we compiling with Clang?
+
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether $CC is Clang" >&5
+$as_echo_n "checking whether $CC is Clang... " >&6; }
+if ${ax_cv_PTHREAD_CLANG+:} false; then :
+  $as_echo_n "(cached) " >&6
 else
-  ax_cv_check_cflags___pthread=no
+  ax_cv_PTHREAD_CLANG=no
+     # Note that Autoconf sets GCC=yes for Clang as well as GCC
+     if test "x$GCC" = "xyes"; then
+        cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+/* Note: Clang 2.7 lacks __clang_[a-z]+__ */
+#            if defined(__clang__) && defined(__llvm__)
+             AX_PTHREAD_CC_IS_CLANG
+#            endif
+
+_ACEOF
+if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
+  $EGREP "AX_PTHREAD_CC_IS_CLANG" >/dev/null 2>&1; then :
+  ax_cv_PTHREAD_CLANG=yes
 fi
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-  CFLAGS=$ax_check_save_flags
+rm -f conftest*
+
+     fi
+
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ax_cv_PTHREAD_CLANG" >&5
+$as_echo "$ax_cv_PTHREAD_CLANG" >&6; }
+ax_pthread_clang="$ax_cv_PTHREAD_CLANG"
+
+
+# GCC generally uses -pthread, or -pthreads on some platforms (e.g. SPARC)
+
+# Note that for GCC and Clang -pthread generally implies -lpthread,
+# except when -nostdlib is passed.
+# This is problematic using libtool to build C++ shared libraries with pthread:
+# [1] https://gcc.gnu.org/bugzilla/show_bug.cgi?id=25460
+# [2] https://bugzilla.redhat.com/show_bug.cgi?id=661333
+# [3] https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=468555
+# To solve this, first try -pthread together with -lpthread for GCC
+
+if test "x$GCC" = "xyes"; then :
+  ax_pthread_flags="-pthread,-lpthread -pthread -pthreads $ax_pthread_flags"
+fi
+
+# Clang takes -pthread (never supported any other flag), but we'll try with -lpthread first
+
+if test "x$ax_pthread_clang" = "xyes"; then :
+  ax_pthread_flags="-pthread,-lpthread -pthread"
 fi
-{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ax_cv_check_cflags___pthread" >&5
-$as_echo "$ax_cv_check_cflags___pthread" >&6; }
-if test "x$ax_cv_check_cflags___pthread" = xyes; then :
 
-    TESTS_CFLAGS="$CFLAGS -pthread"
-    CFLAGS="$CFLAGS -pthread"
 
+# The presence of a feature test macro requesting re-entrant function
+# definitions is, on some systems, a strong hint that pthreads support is
+# correctly enabled
+
+case $host_os in
+        darwin* | hpux* | linux* | osf* | solaris*)
+        ax_pthread_check_macro="_REENTRANT"
+        ;;
+
+        aix*)
+        ax_pthread_check_macro="_THREAD_SAFE"
+        ;;
+
+        *)
+        ax_pthread_check_macro="--"
+        ;;
+esac
+if test "x$ax_pthread_check_macro" = "x--"; then :
+  ax_pthread_check_cond=0
 else
+  ax_pthread_check_cond="!defined($ax_pthread_check_macro)"
+fi
+
+
+if test "x$ax_pthread_ok" = "xno"; then
+for ax_pthread_try_flag in $ax_pthread_flags; do
 
-    { $as_echo "$as_me:${as_lineno-$LINENO}: checking for pthread_create in -lpthread" >&5
-$as_echo_n "checking for pthread_create in -lpthread... " >&6; }
-if ${ac_cv_lib_pthread_pthread_create+:} false; then :
+        case $ax_pthread_try_flag in
+                none)
+                { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether pthreads work without any flags" >&5
+$as_echo_n "checking whether pthreads work without any flags... " >&6; }
+                ;;
+
+                *,*)
+                PTHREAD_CFLAGS=`echo $ax_pthread_try_flag | sed "s/^\(.*\),\(.*\)$/\1/"`
+                PTHREAD_LIBS=`echo $ax_pthread_try_flag | sed "s/^\(.*\),\(.*\)$/\2/"`
+                { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether pthreads work with \"$PTHREAD_CFLAGS\" and \"$PTHREAD_LIBS\"" >&5
+$as_echo_n "checking whether pthreads work with \"$PTHREAD_CFLAGS\" and \"$PTHREAD_LIBS\"... " >&6; }
+                ;;
+
+                -*)
+                { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether pthreads work with $ax_pthread_try_flag" >&5
+$as_echo_n "checking whether pthreads work with $ax_pthread_try_flag... " >&6; }
+                PTHREAD_CFLAGS="$ax_pthread_try_flag"
+                ;;
+
+                pthread-config)
+                # Extract the first word of "pthread-config", so it can be a program name with args.
+set dummy pthread-config; ac_word=$2
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+$as_echo_n "checking for $ac_word... " >&6; }
+if ${ac_cv_prog_ax_pthread_config+:} false; then :
   $as_echo_n "(cached) " >&6
 else
-  ac_check_lib_save_LIBS=$LIBS
-LIBS="-lpthread  $LIBS"
-cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+  if test -n "$ax_pthread_config"; then
+  ac_cv_prog_ax_pthread_config="$ax_pthread_config" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  test -z "$as_dir" && as_dir=.
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then
+    ac_cv_prog_ax_pthread_config="yes"
+    $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+  test -z "$ac_cv_prog_ax_pthread_config" && ac_cv_prog_ax_pthread_config="no"
+fi
+fi
+ax_pthread_config=$ac_cv_prog_ax_pthread_config
+if test -n "$ax_pthread_config"; then
+  { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ax_pthread_config" >&5
+$as_echo "$ax_pthread_config" >&6; }
+else
+  { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
+$as_echo "no" >&6; }
+fi
+
+
+                if test "x$ax_pthread_config" = "xno"; then :
+  continue
+fi
+                PTHREAD_CFLAGS="`pthread-config --cflags`"
+                PTHREAD_LIBS="`pthread-config --ldflags` `pthread-config --libs`"
+                ;;
+
+                *)
+                { $as_echo "$as_me:${as_lineno-$LINENO}: checking for the pthreads library -l$ax_pthread_try_flag" >&5
+$as_echo_n "checking for the pthreads library -l$ax_pthread_try_flag... " >&6; }
+                PTHREAD_LIBS="-l$ax_pthread_try_flag"
+                ;;
+        esac
+
+        ax_pthread_save_CFLAGS="$CFLAGS"
+        ax_pthread_save_LIBS="$LIBS"
+        CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+        LIBS="$PTHREAD_LIBS $LIBS"
+
+        # Check for various functions.  We must include pthread.h,
+        # since some functions may be macros.  (On the Sequent, we
+        # need a special flag -Kthread to make this header compile.)
+        # We check for pthread_join because it is in -lpthread on IRIX
+        # while pthread_create is in libc.  We check for pthread_attr_init
+        # due to DEC craziness with -lpthreads.  We check for
+        # pthread_cleanup_push because it is one of the few pthread
+        # functions on Solaris that doesn't have a non-functional libc stub.
+        # We try pthread_create on general principles.
+
+        cat confdefs.h - <<_ACEOF >conftest.$ac_ext
 /* end confdefs.h.  */
+#include <pthread.h>
+#                       if $ax_pthread_check_cond
+#                        error "$ax_pthread_check_macro must be defined"
+#                       endif
+                        static void *some_global = NULL;
+                        static void routine(void *a)
+                          {
+                             /* To avoid any unused-parameter or
+                                unused-but-set-parameter warning.  */
+                             some_global = a;
+                          }
+                        static void *start_routine(void *a) { return a; }
+int
+main ()
+{
+pthread_t th; pthread_attr_t attr;
+                        pthread_create(&th, 0, start_routine, 0);
+                        pthread_join(th, 0);
+                        pthread_attr_init(&attr);
+                        pthread_cleanup_push(routine, 0);
+                        pthread_cleanup_pop(0) /* ; */
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"; then :
+  ax_pthread_ok=yes
+fi
+rm -f core conftest.err conftest.$ac_objext \
+    conftest$ac_exeext conftest.$ac_ext
 
-/* Override any GCC internal prototype to avoid an error.
-   Use char because int might match the return type of a GCC
-   builtin and then its argument prototype would still apply.  */
-#ifdef __cplusplus
-extern "C"
-#endif
-char pthread_create ();
+        CFLAGS="$ax_pthread_save_CFLAGS"
+        LIBS="$ax_pthread_save_LIBS"
+
+        { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ax_pthread_ok" >&5
+$as_echo "$ax_pthread_ok" >&6; }
+        if test "x$ax_pthread_ok" = "xyes"; then :
+  break
+fi
+
+        PTHREAD_LIBS=""
+        PTHREAD_CFLAGS=""
+done
+fi
+
+
+# Clang needs special handling, because older versions handle the -pthread
+# option in a rather... idiosyncratic way
+
+if test "x$ax_pthread_clang" = "xyes"; then
+
+        # Clang takes -pthread; it has never supported any other flag
+
+        # (Note 1: This will need to be revisited if a system that Clang
+        # supports has POSIX threads in a separate library.  This tends not
+        # to be the way of modern systems, but it's conceivable.)
+
+        # (Note 2: On some systems, notably Darwin, -pthread is not needed
+        # to get POSIX threads support; the API is always present and
+        # active.  We could reasonably leave PTHREAD_CFLAGS empty.  But
+        # -pthread does define _REENTRANT, and while the Darwin headers
+        # ignore this macro, third-party headers might not.)
+
+        # However, older versions of Clang make a point of warning the user
+        # that, in an invocation where only linking and no compilation is
+        # taking place, the -pthread option has no effect ("argument unused
+        # during compilation").  They expect -pthread to be passed in only
+        # when source code is being compiled.
+        #
+        # Problem is, this is at odds with the way Automake and most other
+        # C build frameworks function, which is that the same flags used in
+        # compilation (CFLAGS) are also used in linking.  Many systems
+        # supported by AX_PTHREAD require exactly this for POSIX threads
+        # support, and in fact it is often not straightforward to specify a
+        # flag that is used only in the compilation phase and not in
+        # linking.  Such a scenario is extremely rare in practice.
+        #
+        # Even though use of the -pthread flag in linking would only print
+        # a warning, this can be a nuisance for well-run software projects
+        # that build with -Werror.  So if the active version of Clang has
+        # this misfeature, we search for an option to squash it.
+
+        { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether Clang needs flag to prevent \"argument unused\" warning when linking with -pthread" >&5
+$as_echo_n "checking whether Clang needs flag to prevent \"argument unused\" warning when linking with -pthread... " >&6; }
+if ${ax_cv_PTHREAD_CLANG_NO_WARN_FLAG+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+  ax_cv_PTHREAD_CLANG_NO_WARN_FLAG=unknown
+             # Create an alternate version of $ac_link that compiles and
+             # links in two steps (.c -> .o, .o -> exe) instead of one
+             # (.c -> exe), because the warning occurs only in the second
+             # step
+             ax_pthread_save_ac_link="$ac_link"
+             ax_pthread_sed='s/conftest\.\$ac_ext/conftest.$ac_objext/g'
+             ax_pthread_link_step=`$as_echo "$ac_link" | sed "$ax_pthread_sed"`
+             ax_pthread_2step_ac_link="($ac_compile) && (echo ==== >&5) && ($ax_pthread_link_step)"
+             ax_pthread_save_CFLAGS="$CFLAGS"
+             for ax_pthread_try in '' -Qunused-arguments -Wno-unused-command-line-argument unknown; do
+                if test "x$ax_pthread_try" = "xunknown"; then :
+  break
+fi
+                CFLAGS="-Werror -Wunknown-warning-option $ax_pthread_try -pthread $ax_pthread_save_CFLAGS"
+                ac_link="$ax_pthread_save_ac_link"
+                cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+int main(void){return 0;}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"; then :
+  ac_link="$ax_pthread_2step_ac_link"
+                     cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+int main(void){return 0;}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"; then :
+  break
+fi
+rm -f core conftest.err conftest.$ac_objext \
+    conftest$ac_exeext conftest.$ac_ext
+
+fi
+rm -f core conftest.err conftest.$ac_objext \
+    conftest$ac_exeext conftest.$ac_ext
+             done
+             ac_link="$ax_pthread_save_ac_link"
+             CFLAGS="$ax_pthread_save_CFLAGS"
+             if test "x$ax_pthread_try" = "x"; then :
+  ax_pthread_try=no
+fi
+             ax_cv_PTHREAD_CLANG_NO_WARN_FLAG="$ax_pthread_try"
+
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ax_cv_PTHREAD_CLANG_NO_WARN_FLAG" >&5
+$as_echo "$ax_cv_PTHREAD_CLANG_NO_WARN_FLAG" >&6; }
+
+        case "$ax_cv_PTHREAD_CLANG_NO_WARN_FLAG" in
+                no | unknown) ;;
+                *) PTHREAD_CFLAGS="$ax_cv_PTHREAD_CLANG_NO_WARN_FLAG $PTHREAD_CFLAGS" ;;
+        esac
+
+fi # $ax_pthread_clang = yes
+
+
+
+# Various other checks:
+if test "x$ax_pthread_ok" = "xyes"; then
+        ax_pthread_save_CFLAGS="$CFLAGS"
+        ax_pthread_save_LIBS="$LIBS"
+        CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+        LIBS="$PTHREAD_LIBS $LIBS"
+
+        # Detect AIX lossage: JOINABLE attribute is called UNDETACHED.
+        { $as_echo "$as_me:${as_lineno-$LINENO}: checking for joinable pthread attribute" >&5
+$as_echo_n "checking for joinable pthread attribute... " >&6; }
+if ${ax_cv_PTHREAD_JOINABLE_ATTR+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+  ax_cv_PTHREAD_JOINABLE_ATTR=unknown
+             for ax_pthread_attr in PTHREAD_CREATE_JOINABLE PTHREAD_CREATE_UNDETACHED; do
+                 cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+#include <pthread.h>
 int
 main ()
 {
-return pthread_create ();
+int attr = $ax_pthread_attr; return attr /* ; */
   ;
   return 0;
 }
 _ACEOF
 if ac_fn_c_try_link "$LINENO"; then :
-  ac_cv_lib_pthread_pthread_create=yes
+  ax_cv_PTHREAD_JOINABLE_ATTR=$ax_pthread_attr; break
+fi
+rm -f core conftest.err conftest.$ac_objext \
+    conftest$ac_exeext conftest.$ac_ext
+             done
+
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ax_cv_PTHREAD_JOINABLE_ATTR" >&5
+$as_echo "$ax_cv_PTHREAD_JOINABLE_ATTR" >&6; }
+        if test "x$ax_cv_PTHREAD_JOINABLE_ATTR" != "xunknown" && \
+               test "x$ax_cv_PTHREAD_JOINABLE_ATTR" != "xPTHREAD_CREATE_JOINABLE" && \
+               test "x$ax_pthread_joinable_attr_defined" != "xyes"; then :
+
+cat >>confdefs.h <<_ACEOF
+#define PTHREAD_CREATE_JOINABLE $ax_cv_PTHREAD_JOINABLE_ATTR
+_ACEOF
+
+               ax_pthread_joinable_attr_defined=yes
+
+fi
+
+        { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether more special flags are required for pthreads" >&5
+$as_echo_n "checking whether more special flags are required for pthreads... " >&6; }
+if ${ax_cv_PTHREAD_SPECIAL_FLAGS+:} false; then :
+  $as_echo_n "(cached) " >&6
 else
-  ac_cv_lib_pthread_pthread_create=no
+  ax_cv_PTHREAD_SPECIAL_FLAGS=no
+             case $host_os in
+             solaris*)
+             ax_cv_PTHREAD_SPECIAL_FLAGS="-D_POSIX_PTHREAD_SEMANTICS"
+             ;;
+             esac
+
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ax_cv_PTHREAD_SPECIAL_FLAGS" >&5
+$as_echo "$ax_cv_PTHREAD_SPECIAL_FLAGS" >&6; }
+        if test "x$ax_cv_PTHREAD_SPECIAL_FLAGS" != "xno" && \
+               test "x$ax_pthread_special_flags_added" != "xyes"; then :
+  PTHREAD_CFLAGS="$ax_cv_PTHREAD_SPECIAL_FLAGS $PTHREAD_CFLAGS"
+               ax_pthread_special_flags_added=yes
+fi
+
+        { $as_echo "$as_me:${as_lineno-$LINENO}: checking for PTHREAD_PRIO_INHERIT" >&5
+$as_echo_n "checking for PTHREAD_PRIO_INHERIT... " >&6; }
+if ${ax_cv_PTHREAD_PRIO_INHERIT+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+#include <pthread.h>
+int
+main ()
+{
+int i = PTHREAD_PRIO_INHERIT;
+                                               return i;
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"; then :
+  ax_cv_PTHREAD_PRIO_INHERIT=yes
+else
+  ax_cv_PTHREAD_PRIO_INHERIT=no
 fi
 rm -f core conftest.err conftest.$ac_objext \
     conftest$ac_exeext conftest.$ac_ext
-LIBS=$ac_check_lib_save_LIBS
+
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ax_cv_PTHREAD_PRIO_INHERIT" >&5
+$as_echo "$ax_cv_PTHREAD_PRIO_INHERIT" >&6; }
+        if test "x$ax_cv_PTHREAD_PRIO_INHERIT" = "xyes" && \
+               test "x$ax_pthread_prio_inherit_defined" != "xyes"; then :
+
+$as_echo "#define HAVE_PTHREAD_PRIO_INHERIT 1" >>confdefs.h
+
+               ax_pthread_prio_inherit_defined=yes
+
+fi
+
+        CFLAGS="$ax_pthread_save_CFLAGS"
+        LIBS="$ax_pthread_save_LIBS"
+
+        # More AIX lossage: compile with *_r variant
+        if test "x$GCC" != "xyes"; then
+            case $host_os in
+                aix*)
+                case "x/$CC" in #(
+  x*/c89|x*/c89_128|x*/c99|x*/c99_128|x*/cc|x*/cc128|x*/xlc|x*/xlc_v6|x*/xlc128|x*/xlc128_v6) :
+    #handle absolute path differently from PATH based program lookup
+                     case "x$CC" in #(
+  x/*) :
+
+			   if as_fn_executable_p ${CC}_r; then :
+  PTHREAD_CC="${CC}_r"
+fi
+			   if test "x${CXX}" != "x"; then :
+  if as_fn_executable_p ${CXX}_r; then :
+  PTHREAD_CXX="${CXX}_r"
 fi
-{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_pthread_pthread_create" >&5
-$as_echo "$ac_cv_lib_pthread_pthread_create" >&6; }
-if test "x$ac_cv_lib_pthread_pthread_create" = xyes; then :
-  LIBS="$LIBS -lpthread"
-      ac_fn_c_check_header_mongrel "$LINENO" "pthread.h" "ac_cv_header_pthread_h" "$ac_includes_default"
-if test "x$ac_cv_header_pthread_h" = xyes; then :
+fi
+			  ;; #(
+  *) :
 
+			   for ac_prog in ${CC}_r
+do
+  # Extract the first word of "$ac_prog", so it can be a program name with args.
+set dummy $ac_prog; ac_word=$2
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+$as_echo_n "checking for $ac_word... " >&6; }
+if ${ac_cv_prog_PTHREAD_CC+:} false; then :
+  $as_echo_n "(cached) " >&6
 else
-  as_fn_error $? "pthread.h header not found." "$LINENO" 5
+  if test -n "$PTHREAD_CC"; then
+  ac_cv_prog_PTHREAD_CC="$PTHREAD_CC" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  test -z "$as_dir" && as_dir=.
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then
+    ac_cv_prog_PTHREAD_CC="$ac_prog"
+    $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+PTHREAD_CC=$ac_cv_prog_PTHREAD_CC
+if test -n "$PTHREAD_CC"; then
+  { $as_echo "$as_me:${as_lineno-$LINENO}: result: $PTHREAD_CC" >&5
+$as_echo "$PTHREAD_CC" >&6; }
+else
+  { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
+$as_echo "no" >&6; }
 fi
 
 
+  test -n "$PTHREAD_CC" && break
+done
+test -n "$PTHREAD_CC" || PTHREAD_CC="$CC"
+
+			   if test "x${CXX}" != "x"; then :
+  for ac_prog in ${CXX}_r
+do
+  # Extract the first word of "$ac_prog", so it can be a program name with args.
+set dummy $ac_prog; ac_word=$2
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+$as_echo_n "checking for $ac_word... " >&6; }
+if ${ac_cv_prog_PTHREAD_CXX+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+  if test -n "$PTHREAD_CXX"; then
+  ac_cv_prog_PTHREAD_CXX="$PTHREAD_CXX" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  test -z "$as_dir" && as_dir=.
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then
+    ac_cv_prog_PTHREAD_CXX="$ac_prog"
+    $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
 
+fi
+fi
+PTHREAD_CXX=$ac_cv_prog_PTHREAD_CXX
+if test -n "$PTHREAD_CXX"; then
+  { $as_echo "$as_me:${as_lineno-$LINENO}: result: $PTHREAD_CXX" >&5
+$as_echo "$PTHREAD_CXX" >&6; }
 else
-  as_fn_error $? "libpthread not found" "$LINENO" 5
+  { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
+$as_echo "no" >&6; }
 fi
 
 
+  test -n "$PTHREAD_CXX" && break
+done
+test -n "$PTHREAD_CXX" || PTHREAD_CXX="$CXX"
+
+fi
+
+                      ;;
+esac
+                     ;; #(
+  *) :
+     ;;
+esac
+                ;;
+            esac
+        fi
 fi
 
+test -n "$PTHREAD_CC" || PTHREAD_CC="$CC"
+test -n "$PTHREAD_CXX" || PTHREAD_CXX="$CXX"
+
+
+
+
+
+
+# Finally, execute ACTION-IF-FOUND/ACTION-IF-NOT-FOUND:
+if test "x$ax_pthread_ok" = "xyes"; then
+
+             LIBS="$PTHREAD_LIBS $LIBS"
+             CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+             TESTS_CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+             CC="$PTHREAD_CC"
+
+        :
+else
+        ax_pthread_ok=no
+
+             as_fn_error $? "pthread not found" "$LINENO" 5
+
+fi
+ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+
 
 ac_fn_c_check_func "$LINENO" "pthread_create" "ac_cv_func_pthread_create"
 if test "x$ac_cv_func_pthread_create" = xyes; then :
@@ -8637,34 +9386,33 @@ else
   as_fn_error $? "pthread_mutex_unlock() function not found." "$LINENO" 5
 fi
 
-ac_fn_c_check_func "$LINENO" "pthread_once" "ac_cv_func_pthread_once"
-if test "x$ac_cv_func_pthread_once" = xyes; then :
+ac_fn_c_check_func "$LINENO" "pthread_setspecific" "ac_cv_func_pthread_setspecific"
+if test "x$ac_cv_func_pthread_setspecific" = xyes; then :
 
 else
-  as_fn_error $? "pthread_once() function not found." "$LINENO" 5
+  as_fn_error $? "pthread_setspecific() function not found." "$LINENO" 5
 fi
 
-ac_fn_c_check_func "$LINENO" "pthread_setspecific" "ac_cv_func_pthread_setspecific"
-if test "x$ac_cv_func_pthread_setspecific" = xyes; then :
+ac_fn_c_check_func "$LINENO" "pthread_detach" "ac_cv_func_pthread_detach"
+if test "x$ac_cv_func_pthread_detach" = xyes; then :
 
 else
-  as_fn_error $? "pthread_setspecific() function not found." "$LINENO" 5
+  as_fn_error $? "pthread_detach() function not found." "$LINENO" 5
 fi
 
-ac_fn_c_check_type "$LINENO" "pthread_t" "ac_cv_type_pthread_t" "#include <pthread.h>
-"
-if test "x$ac_cv_type_pthread_t" = xyes; then :
+ac_fn_c_check_func "$LINENO" "pthread_self" "ac_cv_func_pthread_self"
+if test "x$ac_cv_func_pthread_self" = xyes; then :
 
 else
-  as_fn_error $? "pthread_t type not found in pthread.h" "$LINENO" 5
+  as_fn_error $? "pthread_self() function not found." "$LINENO" 5
 fi
 
-ac_fn_c_check_type "$LINENO" "pthread_once_t" "ac_cv_type_pthread_once_t" "#include <pthread.h>
+ac_fn_c_check_type "$LINENO" "pthread_t" "ac_cv_type_pthread_t" "#include <pthread.h>
 "
-if test "x$ac_cv_type_pthread_once_t" = xyes; then :
+if test "x$ac_cv_type_pthread_t" = xyes; then :
 
 else
-  as_fn_error $? "pthread_once_t type not found in pthread.h" "$LINENO" 5
+  as_fn_error $? "pthread_t type not found in pthread.h" "$LINENO" 5
 fi
 
 ac_fn_c_check_type "$LINENO" "pthread_key_t" "ac_cv_type_pthread_key_t" "#include <pthread.h>
@@ -8683,12 +9431,12 @@ else
   as_fn_error $? "pthread_mutex_t type not found in pthread.h" "$LINENO" 5
 fi
 
-ac_fn_c_check_decl "$LINENO" "PTHREAD_ONCE_INIT" "ac_cv_have_decl_PTHREAD_ONCE_INIT" "#include <pthread.h>
+ac_fn_c_check_type "$LINENO" "pthread_cond_t" "ac_cv_type_pthread_cond_t" "#include <pthread.h>
 "
-if test "x$ac_cv_have_decl_PTHREAD_ONCE_INIT" = xyes; then :
+if test "x$ac_cv_type_pthread_cond_t" = xyes; then :
 
 else
-  as_fn_error $? "PTHREAD_ONCE_INIT not found in pthread.h" "$LINENO" 5
+  as_fn_error $? "pthread_cond_t type not found in pthread.h" "$LINENO" 5
 fi
 
 ac_fn_c_check_decl "$LINENO" "PTHREAD_MUTEX_INITIALIZER" "ac_cv_have_decl_PTHREAD_MUTEX_INITIALIZER" "#include <pthread.h>
@@ -8699,6 +9447,14 @@ else
   as_fn_error $? "PTHREAD_MUTEX_INITIALIZER not found in pthread.h" "$LINENO" 5
 fi
 
+ac_fn_c_check_decl "$LINENO" "PTHREAD_COND_INITIALIZER" "ac_cv_have_decl_PTHREAD_COND_INITIALIZER" "#include <pthread.h>
+"
+if test "x$ac_cv_have_decl_PTHREAD_COND_INITIALIZER" = xyes; then :
+
+else
+  as_fn_error $? "PTHREAD_COND_INITIALIZER not found in pthread.h" "$LINENO" 5
+fi
+
 
 curses_lib_name=ncursesw
 
@@ -9886,14 +10642,16 @@ fi
 
 fi
 
+WERROR_TUNING="-Wno-error=deprecated-declarations -Werror=sign-compare -Wno-unused-parameter"
+
 if test "$developer" = "yes"; then
-    CFLAGS="$CFLAGS -g -O0 -Werror -Werror=sign-compare -Wno-unused-parameter"
-    TESTS_CFLAGS="$TESTS_CFLAGS -Werror -Werror=sign-compare -Wno-unused-parameter"
+    CFLAGS="$CFLAGS -g -O0 -Werror $WERROR_TUNING"
+    TESTS_CFLAGS="$TESTS_CFLAGS -Werror $WERROR_TUNING"
 fi
 
 if test "$werror" = "yes"; then
-    CFLAGS="$CFLAGS -Werror"
-    TESTS_CFLAGS="$TESTS_CFLAGS -Werror"
+    CFLAGS="$CFLAGS -Werror $WERROR_TUNING"
+    TESTS_CFLAGS="$TESTS_CFLAGS -Werror $WERROR_TUNING"
 fi
 
 if test "$coverage" = "yes"; then
@@ -10466,7 +11224,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
 # report actual input values of CONFIG_FILES etc. instead of their
 # values after options handling.
 ac_log="
-This file was extended by vifm $as_me 0.12.1, which was
+This file was extended by vifm $as_me 0.13, which was
 generated by GNU Autoconf 2.69.  Invocation command line was
 
   CONFIG_FILES    = $CONFIG_FILES
@@ -10533,7 +11291,7 @@ _ACEOF
 cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
 ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`"
 ac_cs_version="\\
-vifm config.status 0.12.1
+vifm config.status 0.13
 configured by $0, generated by GNU Autoconf 2.69,
   with options \\"\$ac_cs_config\\"
 
diff --git a/configure.ac b/configure.ac
index 5b0b7ec..03ef33c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,6 +1,6 @@
 dnl Process this file with autoconf to produce a configure script.
 
-AC_INIT(vifm, 0.12.1, xaizek@posteo.net, vifm, https://vifm.info)
+AC_INIT(vifm, 0.13, xaizek@posteo.net, vifm, https://vifm.info)
 AC_CONFIG_SRCDIR(src/vifm.c)
 AC_CONFIG_AUX_DIR(build-aux)
 AC_CONFIG_MACRO_DIR(build-aux/m4)
@@ -130,9 +130,6 @@ AC_CHECK_FUNC([close], [], [AC_MSG_ERROR([close() function not found.])])
 AC_CHECK_FUNC([closedir], [], [AC_MSG_ERROR([closedir() function not found.])])
 AC_CHECK_FUNC([dup], [], [AC_MSG_ERROR([dup() function not found.])])
 AC_CHECK_FUNC([dup2], [], [AC_MSG_ERROR([dup2() function not found.])])
-if test -n "$HAVE_MNTENT_H" ; then
-    AC_CHECK_FUNC([endmntent], [], [AC_MSG_ERROR([endmntent() function not found.])])
-fi
 AC_CHECK_FUNC([execve], [], [AC_MSG_ERROR([execve() function not found.])])
 AC_CHECK_FUNC([execvp], [], [AC_MSG_ERROR([execvp() function not found.])])
 AC_CHECK_FUNC([exit], [], [AC_MSG_ERROR([exit() function not found.])])
@@ -147,7 +144,6 @@ AC_CHECK_FUNC([fprintf], [], [AC_MSG_ERROR([fprintf() function not found.])])
 AC_CHECK_FUNC([fputc], [], [AC_MSG_ERROR([fputc() function not found.])])
 AC_CHECK_FUNC([fputs], [], [AC_MSG_ERROR([fputs() function not found.])])
 AC_CHECK_FUNC([free], [], [AC_MSG_ERROR([free() function not found.])])
-AC_CHECK_FUNCS([futimens])
 AC_CHECK_FUNC([fwrite], [], [AC_MSG_ERROR([fwrite() function not found.])])
 AC_CHECK_FUNC([getcwd], [], [AC_MSG_ERROR([getcwd() function not found.])])
 AC_CHECK_FUNC([getenv], [], [AC_MSG_ERROR([getenv() function not found.])])
@@ -156,9 +152,6 @@ AC_CHECK_FUNC([getgrent], [], [AC_MSG_ERROR([getgrent() function not found.])])
 AC_CHECK_FUNC([getgrgid], [], [AC_MSG_ERROR([getgrgid() function not found.])])
 AC_CHECK_FUNC([getgrgid_r], [], [AC_MSG_ERROR([getgrgid_r() function not found.])])
 AC_CHECK_FUNC([getgrnam], [], [AC_MSG_ERROR([getgrnam() function not found.])])
-if test -n "$HAVE_MNTENT_H" ; then
-    AC_CHECK_FUNC([getmntent], [], [AC_MSG_ERROR([getmntent() function not found.])])
-fi
 AC_CHECK_FUNC([getpid], [], [AC_MSG_ERROR([getpid() function not found.])])
 AC_CHECK_FUNC([getppid], [], [AC_MSG_ERROR([getppid() function not found.])])
 AC_CHECK_FUNC([getpwent], [], [AC_MSG_ERROR([getpwent() function not found.])])
@@ -181,6 +174,7 @@ AC_CHECK_FUNC([memccpy], [], [AC_MSG_ERROR([memccpy() function not found.])])
 AC_CHECK_FUNC([memmove], [], [AC_MSG_ERROR([memmove() function not found.])])
 AC_CHECK_FUNC([memset], [], [AC_MSG_ERROR([memset() function not found.])])
 AC_CHECK_FUNC([mkdir], [], [AC_MSG_ERROR([mkdir() function not found.])])
+AC_CHECK_FUNC([mkstemp], [], [AC_MSG_ERROR([mkstemp() function not found.])])
 AC_CHECK_FUNC([opendir], [], [AC_MSG_ERROR([opendir() function not found.])])
 AC_CHECK_FUNC([pathconf], [], [AC_MSG_ERROR([pathconf() function not found.])])
 AC_CHECK_FUNC([pause], [], [AC_MSG_ERROR([pause() function not found.])])
@@ -191,10 +185,10 @@ AC_CHECK_FUNC([popen], [], [AC_MSG_ERROR([popen() function not found.])])
 AC_CHECK_FUNC([printf], [], [AC_MSG_ERROR([printf() function not found.])])
 AC_CHECK_FUNC([puts], [], [AC_MSG_ERROR([puts() function not found.])])
 AC_CHECK_FUNC([qsort], [], [AC_MSG_ERROR([qsort() function not found.])])
+AC_CHECK_FUNC([rand], [], [AC_MSG_ERROR([rand() function not found.])])
 AC_CHECK_FUNC([read], [], [AC_MSG_ERROR([read() function not found.])])
 AC_CHECK_FUNC([readlink], [], [AC_MSG_ERROR([readlink() function not found.])])
 AC_CHECK_FUNC([realloc], [], [AC_MSG_ERROR([realloc() function not found.])])
-AC_CHECK_FUNCS([reallocarray])
 AC_CHECK_FUNC([realpath], [], [AC_MSG_ERROR([realpath() function not found.])])
 AC_CHECK_FUNC([rename], [], [AC_MSG_ERROR([rename() function not found.])])
 AC_CHECK_FUNC([rmdir], [], [AC_MSG_ERROR([rmdir() function not found.])])
@@ -202,9 +196,6 @@ AC_CHECK_FUNC([select], [], [AC_MSG_ERROR([select() function not found.])])
 AC_CHECK_FUNC([setenv], [], [AC_MSG_ERROR([setenv() function not found.])])
 AC_CHECK_FUNC([setgrent], [], [AC_MSG_ERROR([setgrent() function not found.])])
 AC_CHECK_FUNC([setlocale], [], [AC_MSG_ERROR([setlocale() function not found.])])
-if test -n "$HAVE_MNTENT_H" ; then
-    AC_CHECK_FUNC([setmntent], [], [AC_MSG_ERROR([setmntent() function not found.])])
-fi
 AC_CHECK_FUNC([setpgid], [], [AC_MSG_ERROR([setpgid() function not found.])])
 AC_CHECK_FUNC([setpwent], [], [AC_MSG_ERROR([setpwent() function not found.])])
 AC_CHECK_FUNC([setsid], [], [AC_MSG_ERROR([setsid() function not found.])])
@@ -215,6 +206,7 @@ AC_CHECK_FUNC([sigemptyset], [], [AC_MSG_ERROR([sigemptyset() function not found
 AC_CHECK_FUNC([signal], [], [AC_MSG_ERROR([signal() function not found.])])
 AC_CHECK_FUNC([snprintf], [], [AC_MSG_ERROR([snprintf() function not found.])])
 AC_CHECK_FUNC([sprintf], [], [AC_MSG_ERROR([sprintf() function not found.])])
+AC_CHECK_FUNC([srand], [], [AC_MSG_ERROR([srand() function not found.])])
 AC_CHECK_FUNC([strcasecmp], [], [AC_MSG_ERROR([strcasecmp() function not found.])])
 AC_CHECK_FUNC([strcasestr], [AC_DEFINE([HAVE_STRCASESTR], [1], [strcasestr() is available.])], [])
 AC_CHECK_FUNC([strcat], [], [AC_MSG_ERROR([strcat() function not found.])])
@@ -262,6 +254,16 @@ AC_CHECK_FUNC([wcstombs], [], [AC_MSG_ERROR([wcstombs() function not found.])])
 AC_CHECK_FUNC([wcswidth], [], [AC_MSG_ERROR([wcswidth() function not found.])])
 AC_CHECK_FUNC([wcwidth], [], [AC_MSG_ERROR([wcwidth() function not found.])])
 
+AC_CHECK_FUNCS([futimens])
+AC_CHECK_FUNCS([random srandom])
+AC_CHECK_FUNCS([reallocarray])
+
+if test -n "$HAVE_MNTENT_H" ; then
+    AC_CHECK_FUNC([endmntent], [], [AC_MSG_ERROR([endmntent() function not found.])])
+    AC_CHECK_FUNC([getmntent], [], [AC_MSG_ERROR([getmntent() function not found.])])
+    AC_CHECK_FUNC([setmntent], [], [AC_MSG_ERROR([setmntent() function not found.])])
+fi
+
 AC_CHECK_DECLS([_PC_CASE_SENSITIVE], [], [], [[#include <unistd.h>]])
 
 dnl Check for regex.h header, its functions, types and macros.
@@ -320,15 +322,19 @@ AC_CHECK_PROG(COL_PROG, col, col)
 dnl awk is used to generate source with list of documentation tags
 AC_CHECK_PROG(AWK_PROG, awk, awk)
 
-dnl perl is used to generate tags from documentation
-AC_CHECK_PROG(PERL_PROG, perl, perl)
-
 dnl sed is used to make plain text documentation
 AC_CHECK_PROG(SED_PROG, sed, sed)
 
+dnl perl is used to generate tags from documentation
+AC_CHECK_PROG(PERL_PROG, perl, perl)
+
 dnl vim is a fallback for generating documentation tags when perl isn't there
 AC_CHECK_PROG(VIM_PROG, vim, vim)
 
+if test "x${PERL_PROG}${VIM_PROG}" == 'x'; then
+    AC_MSG_ERROR([Either perl or Vim is necessary to generate tags for documentation in Vim's format.])
+fi
+
 dnl check if we're building inside git repository
 AC_CHECK_PROG(GIT_PROG, git, git)
 if test "x${GIT_PROG}" != 'x' -a -d .git/; then
@@ -408,16 +414,14 @@ dnl Enable POSIX shared memory
 AC_CHECK_LIB(rt, shm_open, [LIBS="$LIBS -lrt"])
 
 dnl Use pthread library
-AX_CHECK_COMPILE_FLAG([-pthread], [
-    TESTS_CFLAGS="$CFLAGS -pthread"
-    CFLAGS="$CFLAGS -pthread"
-    ], [
-    AC_CHECK_LIB(pthread, pthread_create,
-      [LIBS="$LIBS -lpthread"
-      AC_CHECK_HEADER([pthread.h], [], [AC_MSG_ERROR([pthread.h header not found.])])
-      ],
-      [AC_MSG_ERROR([libpthread not found])])
-  ])
+AX_PTHREAD([
+             LIBS="$PTHREAD_LIBS $LIBS"
+             CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+             TESTS_CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+             CC="$PTHREAD_CC"
+           ], [
+             AC_MSG_ERROR([pthread not found])
+           ])
 
 dnl Check for all required elements in pthread.h.
 AC_CHECK_FUNC([pthread_create], [], [AC_MSG_ERROR([pthread_create() function not found.])])
@@ -425,14 +429,15 @@ AC_CHECK_FUNC([pthread_getspecific], [], [AC_MSG_ERROR([pthread_getspecific() fu
 AC_CHECK_FUNC([pthread_key_create], [], [AC_MSG_ERROR([pthread_key_create() function not found.])])
 AC_CHECK_FUNC([pthread_mutex_lock], [], [AC_MSG_ERROR([pthread_mutex_lock() function not found.])])
 AC_CHECK_FUNC([pthread_mutex_unlock], [], [AC_MSG_ERROR([pthread_mutex_unlock() function not found.])])
-AC_CHECK_FUNC([pthread_once], [], [AC_MSG_ERROR([pthread_once() function not found.])])
 AC_CHECK_FUNC([pthread_setspecific], [], [AC_MSG_ERROR([pthread_setspecific() function not found.])])
+AC_CHECK_FUNC([pthread_detach], [], [AC_MSG_ERROR([pthread_detach() function not found.])])
+AC_CHECK_FUNC([pthread_self], [], [AC_MSG_ERROR([pthread_self() function not found.])])
 AC_CHECK_TYPE([pthread_t], [], [AC_MSG_ERROR([pthread_t type not found in pthread.h])], [[#include <pthread.h>]])
-AC_CHECK_TYPE([pthread_once_t], [], [AC_MSG_ERROR([pthread_once_t type not found in pthread.h])], [[#include <pthread.h>]])
 AC_CHECK_TYPE([pthread_key_t], [], [AC_MSG_ERROR([pthread_key_t type not found in pthread.h])], [[#include <pthread.h>]])
 AC_CHECK_TYPE([pthread_mutex_t], [], [AC_MSG_ERROR([pthread_mutex_t type not found in pthread.h])], [[#include <pthread.h>]])
-AC_CHECK_DECL([PTHREAD_ONCE_INIT], [], [AC_MSG_ERROR([PTHREAD_ONCE_INIT not found in pthread.h])], [[#include <pthread.h>]])
+AC_CHECK_TYPE([pthread_cond_t], [], [AC_MSG_ERROR([pthread_cond_t type not found in pthread.h])], [[#include <pthread.h>]])
 AC_CHECK_DECL([PTHREAD_MUTEX_INITIALIZER], [], [AC_MSG_ERROR([PTHREAD_MUTEX_INITIALIZER not found in pthread.h])], [[#include <pthread.h>]])
+AC_CHECK_DECL([PTHREAD_COND_INITIALIZER], [], [AC_MSG_ERROR([PTHREAD_COND_INITIALIZER not found in pthread.h])], [[#include <pthread.h>]])
 
 dnl Name of curses library file.
 curses_lib_name=ncursesw
@@ -798,14 +803,16 @@ if test "$use_libX11" = "yes"; then
         ])
 fi
 
+WERROR_TUNING="-Wno-error=deprecated-declarations -Werror=sign-compare -Wno-unused-parameter"
+
 if test "$developer" = "yes"; then
-    CFLAGS="$CFLAGS -g -O0 -Werror -Werror=sign-compare -Wno-unused-parameter"
-    TESTS_CFLAGS="$TESTS_CFLAGS -Werror -Werror=sign-compare -Wno-unused-parameter"
+    CFLAGS="$CFLAGS -g -O0 -Werror $WERROR_TUNING"
+    TESTS_CFLAGS="$TESTS_CFLAGS -Werror $WERROR_TUNING"
 fi
 
 if test "$werror" = "yes"; then
-    CFLAGS="$CFLAGS -Werror"
-    TESTS_CFLAGS="$TESTS_CFLAGS -Werror"
+    CFLAGS="$CFLAGS -Werror $WERROR_TUNING"
+    TESTS_CFLAGS="$TESTS_CFLAGS -Werror $WERROR_TUNING"
 fi
 
 if test "$coverage" = "yes"; then
diff --git a/data/colors/Default-256.vifm b/data/colors/Default-256.vifm
index 8a93538..9502632 100644
--- a/data/colors/Default-256.vifm
+++ b/data/colors/Default-256.vifm
@@ -2,7 +2,7 @@
 
 " This is Vifm's default color scheme for terminals that support 256 colors.
 "
-" This file last updated: 20 August, 2022
+" This file last updated: 2 April, 2023
 
 highlight clear
 
@@ -38,12 +38,12 @@ hi ErrorMsg     cterm=bold                    ctermfg=white    ctermbg=red
 
 hi Directory    cterm=bold                    ctermfg=123      ctermbg=default
 hi Executable   cterm=bold                    ctermfg=119      ctermbg=default
-hi Socket       cterm=bold                    ctermfg=magenta  ctermbg=default
-hi Device       cterm=bold,reverse            ctermfg=red      ctermbg=default
-hi Fifo         cterm=bold,reverse            ctermfg=cyan     ctermbg=default
+hi Socket       cterm=bold,reverse            ctermfg=182      ctermbg=default
+hi Device       cterm=bold,reverse            ctermfg=204      ctermbg=default
+hi Fifo         cterm=bold,reverse            ctermfg=50       ctermbg=default
 hi Link         cterm=bold                    ctermfg=229      ctermbg=default
-hi BrokenLink   cterm=bold                    ctermfg=red      ctermbg=default
-hi HardLink     cterm=bold                    ctermfg=yellow   ctermbg=default
+hi BrokenLink   cterm=bold                    ctermfg=209      ctermbg=default
+hi HardLink     cterm=bold                    ctermfg=221      ctermbg=default
 
 " for powerline-like look of statusline
 highlight User1 ctermbg=white
@@ -53,41 +53,52 @@ highlight User4 ctermbg=white ctermfg=240
 
 " build system files
 highlight {Makefile,Makefile.am,Makefile.in,Makefile.win,*.mak,*.mk,*.m4,*.ac,
-          \configure,CMakeLists.txt,*.cmake,*.pro,*.pri,*.sln}
+          \configure,CMakeLists.txt,*.cmake,*.pro,*.pri,*.sln,meson.build,
+          \meson_options.txt}
         \ cterm=none ctermfg=121 ctermbg=default
 " archives
 highlight {*.7z,*.ace,*.arj,*.bz2,*.cpio,*.deb,*.dz,*.gz,*.jar,*.lzh,*.lzma,
           \*.rar,*.rpm,*.rz,*.tar,*.taz,*.tb2,*.tbz,*.tbz2,*.tgz,*.tlz,*.trz,
-          \*.txz,*.tz,*.tz2,*.xz,*.z,*.zip,*.zoo,*.apk,*.gzip}
+          \*.txz,*.tz,*.tz2,*.xz,*.z,*.zip,*.zoo,*.apk,*.gzip,*.lz}
         \ cterm=none ctermfg=215 ctermbg=default
-" documents, configuration, text-based formats
-highlight {*.djvu,*.htm,*.html,*.shtml,*.css,*.markdown,*.md,*[^0-9].[1-9],
-          \*.mkd,*.org,*.pandoc,*.pdc,*.pdf,*.epub,*.fb2,*.tex,*.txt,*.xhtml,
-          \*.xml,*.pptx,*.ppt,*.doc,*.docx,*.xls,*.xlsm,*.xlsx,*.mobi,*.rtf,
-          \*.less,*.scss,*.log,*.rss,*.xul,*.json,*.yaml,*.yml,*.desktop,*.csv,
-          \*.plist,*.ini,*.cfg,*.rc,*.conf,*.spec,*.qrc,*.odg,*.odt}
+" configuration and other readable textual formats
+highlight {*.css,*.less,*.scss,*.markdown,*.md,*.mkd,*.org,*.pandoc,*.pdc,*.tex,
+          \*.txt,*.xml,*.log,*.rss,*.xul,*.json,*.yaml,*.yml,*.csv,*.plist,
+          \*.ini,*.cfg,*.rc,*.conf,*.spec,*.qrc,*.description,*.list,*.log.old,
+          \*.log.1,*.log.2,*.log.3,*.log.4,*.log.5,*.log.6,*.log.7,*.log.8,
+          \syslog,syslog.1}
         \ cterm=none ctermfg=217 ctermbg=default
-" media
-highlight {*.aac,*.anx,*.asf,*.au,*.avi,*.ts,*.axa,*.axv,*.divx,*.flac,*.m2a,
-          \*.m2v,*.m4a,*.m4p,*.m4v,*.mid,*.midi,*.mka,*.mkv,*.mov,*.mp3,*.mp4,
-          \*.flv,*.mp4v,*.mpc,*.mpeg,*.mpg,*.nuv,*.oga,*.ogg,*.ogv,*.ogx,*.pbm,
-          \*.pgm,*.qt,*.ra,*.ram,*.rm,*.spx,*.vob,*.wav,*.wma,*.wmv,*.xvid,
-          \*.ac3,*.webm,*.cue,*.ape}
+" still text files, but keeping this unoptimizable glob separately
+highlight {*[^0-9].[1-9]}
+        \ cterm=none ctermfg=217 ctermbg=default
+" documents and text-based formats that need special handling
+highlight {*.doc,*.docx,*.xls,*.xlsm,*.xlsx,*.mobi,*.rtf,*.ppt,*.pptx,*.pdf,
+          \*.epub,*.fb2,*.djvu,*.djv,*.htm,*.html,*.shtml,*.xhtml,*.desktop,
+          \*.odg,*.odt,*.ods}
+        \ cterm=none ctermfg=218 ctermbg=default
+" audio
+highlight {*.aac,*.ac3,*.anx,*.ape,*.asf,*.au,*.axa,*.cue,*.flac,*.m2a,*.m4a,
+          \*.mid,*.midi,*.mka,*.mp3,*.mpc,*.mpga,*.oga,*.ogg,*.ogx,*.ra,*.ram,
+          \*.spx,*.wav,*.wma}
+        \ cterm=none ctermfg=79 ctermbg=default
+" multimedia
+highlight {*.avi,*.axv,*.divx,*.flv,*.m2v,*.m4v,*.mp4,*.m4p,*.mp4v,*.mpeg,*.mpg,
+          \*.mkv,*.mov,*.nuv,*.ogv,*.qt,*.rm,*.ts,*.vob,*.webm,*.wmv,*.xvid,
+          \*.unknown_video}
         \ cterm=none ctermfg=49 ctermbg=default
 " images
 highlight {*.bmp,*.gif,*.jpeg,*.jpg,*.ico,*.png,*.ppm,*.svg,*.svgz,*.tga,*.tif,
-          \*.tiff,*.xbm,*.xcf,*.xpm,*.xspf,*.xwd,*.ai}
+          \*.tiff,*.xbm,*.xcf,*.xpm,*.xspf,*.xwd,*.ai,*.pbm,*.pgm,*.webp}
         \ cterm=none ctermfg=117 ctermbg=default
 " executables
 highlight {*.sh,*.bash,*.zsh,*.bat,*.btm,*.cmd,*.com,*.dll,*.exe,*.run,*.msu,
-          \*.msi,*.fish}
+          \*.msi,*.fish,*.AppImage}
         \ cterm=none ctermfg=77 ctermbg=default
 " source code
-highlight {*.patch,*.diff,*.py,*.cpp,*.hpp,*.mk,*.c,*.h,*.cpp,*.hpp,*.cc,*.hh,
+highlight {*.patch,*.diff,*.py,*.cpp,*.hpp,*.c,*.h,*.cpp,*.hpp,*.cc,*.hh,*.ld,
           \*.hs,*.php,*.lua,*.vim,*.vifm,*.asm,*.s,*.java,*.cxx,*.c++,*.go,
           \*.pl,*.pm,*.t,*.cs,*.asp,*.dart,*.js,*.rb,*.scala,*.ts,*.coffee,
-          \*.ml,*.mli,*.rs,*.sql,*.qml,vifmrc,vimrc,.vimrc,*.flex,*.ypp,*.S,
-          \*.ld}
+          \*.ml,*.mli,*.rs,*.sql,*.qml,vifmrc,vimrc,.vimrc,*.flex,*.ypp,*.S}
         \ cterm=none ctermfg=193 ctermbg=default
 " software documentation
 highlight {COPYRIGHT,COPYING*,BUGS,ChangeLog*,FAQ,INSTALL*,LICENCE,LICENSE,NEWS,
diff --git a/data/man/vifm-convert-dircolors.1 b/data/man/vifm-convert-dircolors.1
index 7dfe85d..f975c74 100644
--- a/data/man/vifm-convert-dircolors.1
+++ b/data/man/vifm-convert-dircolors.1
@@ -1,4 +1,4 @@
-.TH VIFM\-CONVERT\-DIRCOLORS 1 "21 September 2022" "vifm 0.12.1"
+.TH VIFM\-CONVERT\-DIRCOLORS 1 "04 April 2023" "vifm 0.13"
 .\" ---------------------------------------------------------------------------
 .SH "NAME"
 .\" ---------------------------------------------------------------------------
diff --git a/data/man/vifm-pause.1 b/data/man/vifm-pause.1
index 2d6b2de..149cbb1 100644
--- a/data/man/vifm-pause.1
+++ b/data/man/vifm-pause.1
@@ -1,4 +1,4 @@
-.TH "VIFM-PAUSE" "1" "21 September 2022" "vifm 0.12.1"
+.TH "VIFM-PAUSE" "1" "04 April 2023" "vifm 0.13"
 .\" ---------------------------------------------------------------------------
 .SH "NAME"
 .\" ---------------------------------------------------------------------------
diff --git a/data/man/vifm-screen-split.1 b/data/man/vifm-screen-split.1
index d88362b..e924eec 100644
--- a/data/man/vifm-screen-split.1
+++ b/data/man/vifm-screen-split.1
@@ -1,4 +1,4 @@
-.TH "VIFM-SCREEN-SPLIT" "1" "21 September 2022" "vifm 0.12.1"
+.TH "VIFM-SCREEN-SPLIT" "1" "04 April 2023" "vifm 0.13"
 .\" ---------------------------------------------------------------------------
 .SH "NAME"
 .\" ---------------------------------------------------------------------------
diff --git a/data/man/vifm.1 b/data/man/vifm.1
index ea6adcb..2d8f058 100644
--- a/data/man/vifm.1
+++ b/data/man/vifm.1
@@ -1,4 +1,4 @@
-.TH VIFM 1 "21 September 2022" "vifm 0.12.1"
+.TH VIFM 1 "04 April 2023" "vifm 0.13"
 .\" ---------------------------------------------------------------------------
 .SH NAME
 .\" ---------------------------------------------------------------------------
@@ -68,6 +68,10 @@ Sets command to be executed on selected files instead of opening them.  The
 command may use any of macros described in "Command macros" section below.  The
 command is executed once for whole selection.
 .TP
+.BI "\-\-plugins\-dir <path>"
+Additional plugins directory (can appear multiple times).  The last one
+added has the highest priority.
+.TP
 .BI "\-\-logging[=<startup log path>]"
 Log some operational details to $XDG_DATA_HOME/vifm/log or $VIFM/log.  If
 the optional startup log path is specified and permissions allow to open
@@ -233,10 +237,10 @@ redraw pane with file in center of list.
 redraw pane with file in bottom of list.
 .TP
 .BI Ctrl-E
-scroll pane one line down.
+scroll pane one line down or column right (in transposed ls-like view).
 .TP
 .BI Ctrl-Y
-scroll pane one line up.
+scroll pane one line up or column left (in transposed ls-like view).
 .\" ---------------------------------------------------------------------------
 .SH Pane manipulation
 .\" ---------------------------------------------------------------------------
@@ -701,9 +705,12 @@ undo last change.
 redo last change.
 .TP
 .BI dp
-in compare view of "ofboth grouppaths" kind, makes corresponding entry of the
-other pane equal to the current one.  The semantics is as follows:
- \- nothing done for identical entries
+in compare view of "ofboth grouppaths" kind makes corresponding entries
+of the other pane equal to the current one.  If at least one file is
+selected, the command processes selection, otherwise current file.
+.br
+The semantics is as follows:
+ \- nothing is done for identical entries
  \- if file is missing in current view, its pair gets removed
  \- if file is missing or differs in other view, it's replaced
  \- file pairs are defined by matching relative paths
@@ -1154,6 +1161,64 @@ insert result of evaluating an expression.  Expression is to be entered
 via nested command-line prompt (where this key does nothing).  Expansion of an
 erroneous expression is empty.
 .\" ---------------------------------------------------------------------------
+.SH Fast navigation
+.\" ---------------------------------------------------------------------------
+In order to streamline navigation through directory tree, you can enter a
+special form of command-line mode from search or local filter prompt.  Once
+activated, pressing Enter opens currently selected directory and clears the
+prompt in anticipation of the next component of the path.  If entry under the
+cursor is a file, it is opened and the mode is finished.
+
+This behaviour is embedded in a command-line mode, but doesn't update input
+histories nor expands abbreviations and redefines some of the mode's mappings
+for the purpose of faster navigation through the file system rather than
+command-line editing.  When on, prompt gets "nav" prefix.
+
+You can enable this behaviour on search by default via a mapping like:
+.EX
+
+    nnoremap / /<c-y>
+
+.EE
+.TP
+.BI Ctrl-Y
+enter navigation mode.  Works only for search and local filter started
+from a normal mode and only when 'incsearch' is set ('wrapscan' is also nice to
+have set for search).
+.TP
+.BI Ctrl-Y
+return to a regular command-line mode.
+.TP
+.BI "Enter, Right"
+either enter a directory under the cursor without leaving the mode and
+clear the prompt or leave the mode for files.  If 'navoptions'
+specifies "open:all" a file under the cursor is opened after leaving the
+mode.
+.TP
+.BI "Ctrl-O, Left"
+go to parent directory.
+.TP
+.BI "Ctrl-J"
+leave the mode without undoing cursor position or filter state.
+.TP
+.BI "Ctrl-N, Down"
+move view cursor down.
+.TP
+.BI "Ctrl-P, Up"
+move view cursor up.
+.TP
+.BI "Page Down"
+scroll view down.
+.TP
+.BI "Page Up"
+scroll view up.
+.TP
+.BI Home
+move view cursor to the first item.
+.TP
+.BI End
+move view cursor to the last item.
+.\" ---------------------------------------------------------------------------
 .SH Pasting special values
 .\" ---------------------------------------------------------------------------
 The shortcuts listed below insert specified values into current cursor
@@ -1336,6 +1401,8 @@ line.  If you want to use '|' in an argument, precede it with '\\'.
 These commands see '|' as part of their arguments even when it's escaped:
 
     :[range]!
+    :amap
+    :anoremap
     :autocmd
     :cabbrev
     :cmap
@@ -1729,13 +1796,29 @@ equals
 .TP
 .BI "                                         :compare"
 .TP
-.BI ":compare [byname | bysize | bycontents | listall | listunique | listdups |\
- ofboth | ofone | groupids | grouppaths | skipempty]..."
+.BI ":compare [byname | bysize | bycontents |"
+.BI "   listall | listunique | listdups |"
+.br
+.BI "   ofboth | ofone |"
+.br
+.BI "   groupids | grouppaths |"
+.br
+.BI "   skipempty | withicase | withrcase |"
+.br
+.BI "   showidentical | showdifferent | showuniqueleft | showuniqueright]..."
+.br
 compare files in one or two views according to the arguments.  The default
-is "bycontents listall ofboth grouppaths".  See "Compare views" section below
+is "bycontents listall ofboth grouppaths showidentical showdifferent
+showuniqueleft showuniqueright".  See "Compare views" section below
 for details.  Diff structure is incompatible with alternative representations,
 so values of 'lsview' and 'millerview' options are ignored.
 .TP
+.BI ":compare! (showidentical | showdifferent | showuniqueleft |"
+.BI "    showuniqueright)..."
+this invocation form works only when compare view is active and results in
+redoing of the previous :compare with toggled state of the passed in
+options.
+.TP
 .BI "                                         :copen"
 .TP
 .BI ":cope[n]"
@@ -2177,7 +2260,7 @@ their content (e.g. regular files in views)
  \- CmpMismatch \- color of mismatched files in side-by-side comparison by path
  \- CmpUnmatched \- comparison file entry that has no pair in the other pane
  \- CmpBlank \- entry placeholder in a compare view, paired with CmpUnmatched
- \- User1..User9 \- 9 colors which can be used via %* 'statusline' macro
+ \- User1..User20 \- 20 colors which can be used via %* 'statusline' macro
 
 Available colors:
  \- \-1 or default or none \- default or transparent
@@ -2323,7 +2406,7 @@ transparency:
   SuggestBox
   StatusLine
     WildMenu
-    User1..User9
+    User1..User20
   Border
   CmdLine
     ErrorMsg
@@ -2351,10 +2434,10 @@ transparency:
   TopLine
     TopLineSel
       TabLineSel (for pane tabs)
-        User1..User9
+        User1..User20
   TabLine
     TabLineSel
-      User1..User9
+      User1..User20
 
 "none" means default terminal color for highlight groups at the first level of
 the hierarchy and transparency for all others.
@@ -2650,6 +2733,15 @@ commands).
 .BI :redr[aw]
 redraw the screen immediately.
 .TP
+.BI "                                         :regedit"
+.TP
+.BI ":rege[dit] [{reg}]"
+edit register contents using external editor. If {reg} is
+omitted, unnamed register will be edited by default.
+Edited paths are normalized (no extra `.`, `..`, `/`, etc.) and all
+relative paths are treated as starting in the directory of the current
+view.
+.TP
 .BI "                                         :registers"
 .TP
 .BI :reg[isters]
@@ -3176,7 +3268,10 @@ map lhs key sequence to rhs in normal and visual modes.
 map lhs key sequence to rhs in command line mode.
 
 .TP
-.BI "                                       :cmap :dmap :mmap :nmap :qmap :vmap"
+.BI "                                 :amap :cmap :dmap :mmap :nmap :qmap :vmap"
+.TP
+.BI ":amap lhs rhs"
+map lhs to rhs in navigation mode.
 .TP
 .BI ":cm[ap] lhs rhs"
 map lhs to rhs in command line mode.
@@ -3199,6 +3294,9 @@ map lhs to rhs in visual mode.
 .TP
 .BI "                                         :*map"
 .TP
+.BI :amap
+list all maps in navigation mode.
+.TP
 .BI :cm[ap]
 list all maps in command line mode.
 .TP
@@ -3219,6 +3317,9 @@ list all maps in visual mode.
 .TP
 .BI "                                         :*map beginning"
 .TP
+.BI ":amap beginning"
+list all maps in navigation mode that start with the beginning.
+.TP
 .BI ":cm[ap] beginning"
 list all maps in command line mode that start with the beginning.
 .TP
@@ -3247,7 +3348,11 @@ user mappings in rhs.
 map the key sequence lhs to rhs for command line mode, but don't expand user
 mappings in rhs.
 .TP
-.BI "               :cnoremap :dnoremap :mnoremap :nnoremap :qnoremap :vnoremap"
+.BI "     :anoremap :cnoremap :dnoremap :mnoremap :nnoremap :qnoremap :vnoremap"
+.TP
+.BI ":anoremap lhs rhs"
+map the key sequence lhs to rhs for navigation mode, but don't expand user
+mappings in rhs.
 .TP
 .BI ":cno[remap] lhs rhs"
 map the key sequence lhs to rhs for command line mode, but don't expand user
@@ -3281,7 +3386,10 @@ remove user mapping of lhs from normal and visual modes.
 .BI ":unm[ap]! lhs"
 remove user mapping of lhs from command line mode.
 .TP
-.BI "                           :cunmap :dunmap :munmap :nunmap :qunmap :vunmap"
+.BI "                   :aunmap :cunmap :dunmap :munmap :nunmap :qunmap :vunmap"
+.TP
+.BI ":aunmap lhs"
+remove user mapping of lhs from navigation mode.
 .TP
 .BI ":cu[nmap] lhs"
 remove user mapping of lhs from command line mode.
@@ -3437,11 +3545,13 @@ Same as %s, but splits vertically.
 Forbid use of terminal multiplexer to run the command.
 .TP
 .BI %N
-Do not detach viewer from terminal session (leaves `/dev/tty` available).
+Do not detach viewer from terminal session (keeps `/dev/tty`
+available) or process group (keeps the command in the set of
+foreground clients of the terminal).
 .TP
 .BI %i
-Completely ignore command output.  For background jobs this suppresses error
-dialogs, while still storing errors internally for viewing via :jobs menu.
+Run in background and suppress error dialogs, but collect
+errors internally for viewing via :jobs menu.
 .TP
 .BI %Pl
 Pipe list of files to standard input of a command.
@@ -4374,6 +4484,7 @@ scope: local
 Configures ls-like view.
 
   item          used for
+  columncount   fixed number of columns to display or 0
   transposed    filling view grid by columns rather than by lines
 
 .TP
@@ -4410,8 +4521,8 @@ Configures miller view.
 column, but central (main) column can't be disabled.
 
 rpreview specifies what file-system objects should be previewed in the right
-column and can take two values: dirs (only directories) or all.  Both options
-don't include parent directory ("..").
+column and takes three values: dirs (only directories), files (only files) or
+all.  Neither value enables preview of parent directory ("..").
 
 Example of two-column mode which is useful in combination with :view command:
 .EX
@@ -4441,6 +4552,34 @@ made by external applications, monitoring background jobs, redrawing UI).  There
 are no strict guarantees, however the higher this value is, the less is CPU load
 in idle mode.
 .TP
+.BI "'mouse'"
+type: charset
+.br
+default: ""
+.br
+
+Contains a sequence of single-character flags:
+ - a - all supported modes (a shorthand for all the rest and future additions)
+ - c - command-line mode (includes navigation mode)
+ - m - menu mode
+ - n - normal mode
+ - q - view mode
+ - v - visual mode
+.TP
+.BI 'navoptions'
+type: string list
+.br
+default: "open:dirs"
+.br
+
+Configures behaviour of navigation mode.
+
+  item      default
+  open:str  dirs
+
+The "open" item specifies what file-system objects should be opened on
+Enter and can take two values: dirs (only directories) or all.
+.TP
 .BI "'number' 'nu'"
 type: boolean
 .br
@@ -4895,9 +5034,9 @@ since it cannot be selected
 .IP \- 2
 %{<expr>} - evaluate arbitrary vifm expression '<expr>', e.g. '&sort'
 .IP \- 2
-%* - resets or applies one of User1..User9 highlight groups; reset happens
-when width field is 0 or not specified, one of groups gets picked
-when width field is in the range from 1 to 9
+%* - resets or applies one of User1..User20 highlight groups; reset happens
+when width field is 0 or not specified, one of the groups gets picked
+when width field is in the range from 1 to 20
 .IP \- 2
 all 'rulerformat' macros
 .RE
@@ -4991,7 +5130,8 @@ type: string
 .br
 default: ""
 .br
-When non-empty, determines format of the main part of a single tab's label.
+When non-empty and 'tabline' isn't set, determines format of the main
+part of a single tab's label.
 
 When empty, tab label is set to either tab name for named tabs or to view
 title (usually current path) for unnamed tabs.
@@ -4999,29 +5139,29 @@ title (usually current path) for unnamed tabs.
 The following macros can appear in the format (see below for what a flag is):
 .RS
 .IP \- 2
-%C      \- flag of a current tab
+%C       \- flag of a current tab
 .IP \- 2
-%N      \- number of the tab
+%N       \- number of the tab
 .IP \- 2
-%T      \- flag of a tree mode
+%T       \- flag of a tree mode
 .IP \- 2
-%c      \- description of a custom view
+%c       \- description of a custom view
 .IP \- 2
-%n      \- name of the tab
+%n       \- name of the tab
 .IP \- 2
-%p      \- path of the view (handles filename modifiers)
+%p       \- path of the view (handles filename modifiers)
 .IP \- 2
-%t      \- title of the view (affected by 'shortmess' flags)
+%t       \- title of the view (affected by 'shortmess' flags)
 .IP \- 2
-%%      \- literal percent sign
+%%       \- literal percent sign
 .IP \- 2
-%[      \- designates beginning of an optional block
+%[       \- designates beginning of an optional block
 .IP \- 2
-%]      \- designates end of an optional block
+%]       \- designates end of an optional block
 .IP \- 2
-%*, %0* \- resets highlighting
+%*, %0*  \- resets highlighting
 .IP \- 2
-%1-%9   \- applies one of User1..User9 highlight groups
+%1*-%20* \- applies one of User1..User20 highlight groups
 .RE
 
 .RS
@@ -5029,7 +5169,7 @@ In global tabs the view in bullets above refers to currently active view of that
 tab.
 
 Flag macros are a special kind of macros that always expand to an empty value
-and are ment to be used inside optional blocks to control their visibility.
+and are meant to be used inside optional blocks to control their visibility.
 
 Optional blocks are ignored unless at least one macro inside of them is
 expanded to a non-empty value or is a set flag macro.
@@ -5046,6 +5186,22 @@ expanded to a non-empty value or is a set flag macro.
 .EE
 .RE
 .TP
+.BI "'tabline' 'tal'"
+type: string
+.br
+default: ""
+.br
+When non-empty, determines format of the tab line.  Note that mouse clicks
+won't be handled when this option is non-empty.
+
+The following macros can appear in the format:
+.RS
+.IP \- 2
+%*, %0*  \- resets highlighting
+.IP \- 2
+%1*-%20* \- applies one of User1..User20 highlight groups
+.RE
+.TP
 .BI 'tabprefix'
 type: string
 .br
@@ -5332,6 +5488,19 @@ Searches wrap around end of the list.
 .\" ---------------------------------------------------------------------------
 .SH Mappings
 .\" ---------------------------------------------------------------------------
+A user mapping like `nnoremap lhs rhs` defines a substitution of the
+left-hand-side (LHS) with the right-hand-side (RHS) in the input stream.  A
+regular mapping (without "nore" in :command's name) expands recognized
+sequences in the RHS, while "*noremap" mapping always interprets RHS as if no
+user mappings were defined and each key has its builtin meaning.  In most cases
+you want to use noremap variant and if your RHS includes LHS, only noremap
+variant will work because recursion in a mapping is not allowed.
+
+In order to define a mapping determine in which mode you want to activate it
+and use an appropriate "*noremap" :command (e.g., :nnoremap for a
+normal mode mapping).  RHS doesn't have to limit itself to the mode in which
+the mapping was started and can span multiple modes.
+
 .B Map arguments
 
 LHS of mappings can be preceded by arguments which take the form of special
@@ -5836,6 +6005,9 @@ extcached({cache}, {path}, {extcmd})
                       String      Caches output of {extcmd} per {cache} and
                                   {path} combination.
 .br
+filereadable({path})  Integer     Checks whether {expr} points to a
+                                  non-directory that can be read.
+.br
 filetype({fnum} [, {resolve}])
                       String      Returns file type from position.
 .br
@@ -5845,6 +6017,10 @@ getpanetype()         String      Returns type of current pane.
 .br
 has({property})       Integer     Checks whether instance has {property}.
 .br
+input({prompt} [, {initial} [, {completion}]])
+.br
+                      String      Prompts user for an input on command-line.
+.br
 layoutis({type})      Integer     Checks whether layout is of type {type}.
 .br
 paneisat({loc})       Integer     Checks whether current pane is at {loc}.
@@ -5921,6 +6097,12 @@ Example:
                                      \ expand('stat \-\-format=%%bx%%B %c')) }"
 .EE
 
+.BI filereadable({path})
+
+Checks whether {path} exists and refers to a non-directory entry and its
+permissions allow reading.  Returns boolean value describing result of the
+check.
+
 .BI "filetype({fnum} [, {resolve}])"
 
 The result is a string, which represents file type and is one of the list:
@@ -5964,6 +6146,7 @@ Retrieves string describing type of current pane.  Possible return values:
     custom       custom file list (%u)
     very-custom  very custom file list (%U)
     tree         tree view
+    compare      compare view
 
 .BI has({property})
 
@@ -5987,6 +6170,32 @@ Usage example:
   execute 'set' 'statusline="  %t%= %A '.$RIGHTS.'%15E %20d  "'
 .EE
 
+.BI "input({prompt} [, {initial} [, {completion}]])"
+
+Creates a command-line prompt to obtain user's input.  Initial value can
+be supplied as an optional second parameter, otherwise empty string is used.
+
+Optional third parameter specifies kind of completion, which can be one of:
+    dir   paths to directories
+    file  paths to files and directories
+    ""    (empty string, default) no completion
+
+Note that behaviour differs from Vim where executing a mapping like
+.EX
+  nnoremap j :echo input('text: ')<cr>input
+.EE
+leaves you in a prompt mode with "input" typed in.  Vifm will wait for leaving
+the prompt and then continue executing the mapping.
+
+Usage example:
+
+.EX
+  nnoremap ,m : let $DIR_NAME = input('mkdir: ', '', 'dir')
+             \\| if $DIR_NAME != ''
+             \\|     execute 'mkdir' fnameescape($DIR_NAME)
+             \\| endif<cr>
+.EE
+
 .BI layoutis({type})
 
 Checks whether current interface layout is {type} or not, where {type} can be:
@@ -6269,17 +6478,19 @@ argument for the chmod program.  If you're not on OS X and want to remove \
 execute permission bit from all files, but preserve it for directories, set \
 all execute flags to 'd' and check \(aqSet Recursively\(aq flag.
 .LP
-.B Jobs menu
+.B Jobs menu (:jobs)
 
-dd requests cancellation of job under cursor.  The job won't be removed from
+dd - request cancellation of job under cursor.  The job won't be removed from
 the list, but marked as being cancelled (if cancellation was successfully
 requested).  A message will pop up if the job has already stopped.
 Note that on Windows cancelling external programs like this might not work,
 because their parent shell doesn't have any windows.
 
-e key displays errors of selected job if any were collected.  They are
+e - display errors of selected job if any were collected.  They are
 displayed in a new menu, but you can get back to jobs menu by pressing h.
 
+r - reload the list of jobs.
+
 .LP
 .B Undolist menu
 
@@ -6418,7 +6629,7 @@ grouping to preserve as all file ids are guaranteed to be distinct.
 
 .B Creation
 
-Arguments passed to :compare form four categories each with its own
+Arguments passed to :compare form seven categories each with its own
 prefix and is responsible for particular property of operation.
 
 Which files to compare:
@@ -6445,9 +6656,22 @@ displaying identically named files as mismatches).
 Which files to omit:
  \- skipempty \- ignore empty files.
 
+Comparison tweaks:
+ \- withicase \- ignore case when comparing file names/paths;
+ \- withrcase \- respect case when comparing file names/paths.
+
+Which results to show (has no effect for single pane comparison):
+ \- showidentical   \- toggle showing of identical files;
+ \- showdifferent   \- toggle showing of different files;
+ \- showuniqueleft  \- toggle showing of unique top/left files;
+ \- showuniqueright \- toggle showing of unique bottom/right files.
+
 Each argument can appear multiple times, the rightmost one of the group is
 considered.  Arguments alter default behaviour instead of substituting it.
 
+When neither "withicase" nor "withrcase" is specified, case depends on the
+running operating system and the file system on which the files are located.
+
 .B Examples
 
 The defaults corresponds to probably the most common use case of comparing
@@ -6457,6 +6681,7 @@ files in two trees with grouping by paths, so the following are equivalent:
   :compare
   :compare bycontents grouppaths
   :compare bycontents listall ofboth grouppaths
+  :compare showidentical showdifferent showuniqueleft showuniqueright
 .EE
 
 Another use case is to find duplicates in the current sub-tree:
@@ -6479,6 +6704,9 @@ data.
 Comparison views have second column displaying id of the file, files with same
 id are considered to be equal.  The view columns configuration is predefined.
 
+The status bar displays only the initial result of the comparison and can be
+out of date.
+
 .B Behaviour
 
 When two views are being compared against each other the following changes to
@@ -7140,6 +7368,82 @@ vertical split.
 
 You can cancel renaming by removing all non-comments from the buffer.  This
 also erases information about previous edits.
+.\" ---------------------------------------------------------------------------
+.SH Using mouse
+.\" ---------------------------------------------------------------------------
+Note: <ScrollWheelDown> is not available on 32-bit *nix systems, because
+ncurses doesn't support it there (limitation of implementation).
+
+Note: these are not available in mappings at the momemnt.
+
+.B "Normal Mode"
+
+  event             position  change  action
+                     cursor   window
+  <LeftMouse>         yes      yes    <cr> if cursor wasn't move
+  <LeftRelease>        no      yes
+  <MiddleMouse>        no      yes    <c-e>
+  <MiddleRelease>      no      yes
+  <RightMouse>        yes      yes    :file
+  <RightRelease>       no      yes
+  <ScrollWheelUp>      no      yes    <c-y> or :tabprevious
+  <ScrollWheelDown>    no      yes    <c-e> or :tabnext
+
+Clicking on or scrolling over an inactive pane (including its title), makes it
+active and does nothing else.  Tabs are scrolled when mouse hovers over them.
+
+Clicking on the left miller column goes to parent directory and clicking the
+right one opens current entry.
+
+.B "Visual Mode"
+
+  event             position  selection  action
+                     cursor
+  <LeftMouse>         yes      update    <cr> if cursor wasn't move
+  <LeftRelease>        no
+  <MiddleMouse>        no      update    <c-e>
+  <MiddleRelease>      no
+  <RightMouse>         no
+  <RightRelease>       no
+  <ScrollWheelUp>      no      update    <c-y>
+  <ScrollWheelDown>    no      update    <c-e>
+
+.B "Command-line Mode"
+
+  event             position  action
+                     cursor
+  <LeftMouse>         yes
+  <LeftRelease>        no
+  <MiddleMouse>        no     <c-n>
+  <MiddleRelease>      no
+  <RightMouse>         no
+  <RightRelease>       no
+  <ScrollWheelUp>      no     <c-p>
+  <ScrollWheelDown>    no     <c-n>
+
+.B "Menu Mode"
+
+  event             position  action
+                     cursor
+  <LeftMouse>         yes     <cr> if cursor wasn't moved
+  <LeftRelease>        no
+  <MiddleMouse>        no     <c-e>
+  <MiddleRelease>      no
+  <RightMouse>         no
+  <RightRelease>       no
+  <ScrollWheelUp>      no     <c-y>
+  <ScrollWheelDown>    no     <c-e>
+
+.B "view Mode"
+
+  event               action
+
+  <ScrollWheelUp>     k
+  <ScrollWheelDown>   j
+
+Clicking on or scrolling over an inactive pane (including its title), detaches
+view mode if it wasn't activated for exploring a file.
+
 .\" ---------------------------------------------------------------------------
 .SH Plugin
 .\" ---------------------------------------------------------------------------
diff --git a/data/plugins/operator-select/init.lua b/data/plugins/operator-select/init.lua
new file mode 100644
index 0000000..e602c0c
--- /dev/null
+++ b/data/plugins/operator-select/init.lua
@@ -0,0 +1,51 @@
+--[[
+
+Adds mappings that select entries in normal mode.
+
+Usage:
+
+    s<selector>, e.g., `sa` selects all entries in a view.
+    [count]ss selects [count] entries next to the cursor.
+
+--]]
+
+local M = {}
+
+local added = vifm.keys.add {
+    shortcut = 's',
+    description = 'select entries',
+    modes = { 'normal' },
+    followedby = 'selector',
+    handler = function(info)
+        vifm.currview():select({ indexes = info.indexes })
+    end,
+}
+if not added then
+    vifm.sb.error('Failed to register s')
+end
+
+local function range(begin, end_)
+    local ints = {}
+    for i = begin, end_ do
+        table.insert(ints, i)
+    end
+    return ints
+end
+
+local added = vifm.keys.add {
+    shortcut = 'ss',
+    description = 'select [count] entries',
+    modes = { 'normal' },
+    handler = function(info)
+        local count = info.count or 1
+        local currview = vifm.currview()
+        local indexes = range(currview.currententry,
+                              currview.currententry + count - 1)
+        currview:select({ indexes = indexes })
+    end,
+}
+if not added then
+    vifm.sb.error('Failed to register ss')
+end
+
+return M
diff --git a/data/plugins/statusbar/init.lua b/data/plugins/statusbar/init.lua
new file mode 100644
index 0000000..c743d30
--- /dev/null
+++ b/data/plugins/statusbar/init.lua
@@ -0,0 +1,31 @@
+--[[
+
+Example of displaying target of symbolic links on statusline.
+
+Usage example for `vifmrc`:
+
+    set statusline=#statusbar#fmt
+
+--]]
+
+local function fmt(info)
+    local view = info.view
+    local entry = view:entry(view.currententry)
+    local format = " %t%=%A %10u:%-7g %15s %20d "
+    if entry.type == 'link' then
+        local target = entry.gettarget()
+        target = string.gsub(target, "%%", "%%%%")
+        format = " %t -> " .. target .. " %= %A %10u:%-7g %15s %20d "
+    end
+    return { format = format }
+end
+
+local added = vifm.addhandler {
+    name = "fmt",
+    handler = fmt,
+}
+if not added then
+    vifm.sb.error("Failed to register #%s#fmt", vifm.plugin.name)
+end
+
+return {}
diff --git a/data/plugins/tabline/init.lua b/data/plugins/tabline/init.lua
new file mode 100644
index 0000000..5d92cad
--- /dev/null
+++ b/data/plugins/tabline/init.lua
@@ -0,0 +1,83 @@
+--[[
+
+Example of a custom powerline-like tab line.
+
+Usage example for `vifmrc`:
+
+    set tabline=#tabline#fmt
+
+User colors for a color scheme:
+
+    " colors:
+    " - active tab bg   = 231
+    " - active tab fg   = 232
+    " - inactive tab bg = 237
+    " - inactive tab fg = 7
+    hi User11  ctermfg=232  ctermbg=231  cterm=bold " active start
+    hi User12  ctermfg=231  ctermbg=237  cterm=bold " active-to-inactive end
+    hi User13  ctermfg=231  ctermbg=-1   cterm=bold " last active end
+    hi User14  ctermfg=7    ctermbg=237  cterm=bold " inactive start
+    hi User15  ctermfg=237  ctermbg=231  cterm=bold " before active end
+    hi User16  ctermfg=237  ctermbg=-1   cterm=bold " last inactive end
+
+--]]
+
+local function fmt(info)
+    local ntabs = vifm.tabs.getcount({ other = info.other })
+    local ctab = vifm.tabs.getcurrent({ other = info.other })
+    local format = ''
+    for idx = 1, ntabs do
+        local tab = vifm.tabs.get({ index = idx, other = info.other })
+        local view = tab:getview()
+
+        local name = tab:getname()
+        if #name == 0 then
+            name = vifm.fnamemodify(view.cwd, ':t') .. '/'
+        end
+
+        local cv = view.custom
+        if cv ~= nil then
+            local title = cv.title
+            if cv.type == 'tree' then
+                title = 'tree'
+            end
+            name = string.format('[%s] %s', title, name)
+        end
+
+        name = string.gsub(name, '%%', '%%%%')
+
+        local islast = (idx == ntabs)
+        local active = (idx == ctab)
+
+        local startc = active and 11 or 14
+
+        local endc
+        if active then
+            endc = islast and 13 or 12
+        else
+            endc = islast and 16 or 15
+        end
+
+        -- special case: inactive tab followed by another inactive tab
+        local tail = ''
+        if not active and not islast and idx ~= ctab - 1 then
+            tail = ''
+            endc = 14
+        end
+
+        local label = string.format('%%%d* %d: %s %%%d*%s',
+                                    startc, idx, name, endc, tail)
+        format = format .. label
+    end
+    return { format = format }
+end
+
+local added = vifm.addhandler {
+    name = 'fmt',
+    handler = fmt,
+}
+if not added then
+    vifm.sb.error('Failed to register #%s#fmt', vifm.plugin.name)
+end
+
+return {}
diff --git a/data/plugins/ueberzug/init.lua b/data/plugins/ueberzug/init.lua
index a7ea176..c9663e5 100644
--- a/data/plugins/ueberzug/init.lua
+++ b/data/plugins/ueberzug/init.lua
@@ -1,6 +1,9 @@
 --[[
 
-Enables use of Ueberzug for viewing images.
+Enables use of Ueberzug for viewing images:
+ - starts Ueberzug on the first use
+ - prints an error message to preview area if command isn't found
+ - restarts Ueberzug if it exits (you also get an error message about it)
 
 Usage example:
 
@@ -9,26 +12,50 @@ Usage example:
               \ %pc
               \ #ueberzug#clear
 
-Ueberzug: https://github.com/seebye/ueberzug/
+Ueberzug:
+ - original (discontinued): https://github.com/seebye/ueberzug/
+ - fork of python version:  https://github.com/ueber-devel/ueberzug
+ - improved rewrite in C++: https://github.com/jstkdng/ueberzugpp
 
 --]]
 
--- TODO: check that `ueberzug` is available
+-- TODO: maybe don't register handlers if executable isn't present to allow
+--       other viewers to kick in
 -- TODO: generate JSON properly
 -- TODO: handle other types of files
--- TODO: launch Ueberzug on first use
+-- TODO: stop Ueberzug after some period of inactivity (needs timer API?)
 
 local M = { }
-
-local uberzug = vifm.startjob {
-    cmd = 'ueberzug layer --silent',
-    iomode = 'w'
-}
-
-local pipe = uberzug:stdin()
 local layer_id = "vifm-preview"
 
+local pipe_storage
+local function get_pipe()
+    if pipe_storage == nil then
+        if not vifm.executable('ueberzug') then
+            return nil, "ueberzug executable isn't found"
+        end
+
+        local uberzug = vifm.startjob {
+            cmd = 'ueberzug layer',
+            iomode = 'w',
+            onexit = function()
+                pipe_storage = nil
+                vifm.errordialog('ueberzug plugin',
+                                 'ueberzug has exited unexpectedly.\n'..
+                                 'It will be restarted on the next use.')
+            end
+        }
+        pipe_storage = uberzug:stdin()
+    end
+    return pipe_storage
+end
+
 local function view(info)
+    local pipe, err = get_pipe()
+    if pipe == nil then
+        return { lines = { err } }
+    end
+
     local format = '{ "action":"add", "identifier":"%s",'
                  ..'  "x":%d, "y":%d, "width":%d, "height":%d,'
                  ..'  "path":"%s"'
@@ -44,6 +71,11 @@ local function view(info)
 end
 
 local function clear(info)
+    local pipe, err = get_pipe()
+    if pipe == nil then
+        return { lines = { err } }
+    end
+
     local format = '{"action":"remove", "identifier":"%s"}\n'
 
     local message = format:format(layer_id)
diff --git a/data/plugins/unpack/init.lua b/data/plugins/unpack/init.lua
index 1e06487..7fb55c2 100644
--- a/data/plugins/unpack/init.lua
+++ b/data/plugins/unpack/init.lua
@@ -13,7 +13,6 @@ Usage example:
 -- TODO: a way to specify default path for unpacking
 -- TODO: fix processing archives with spaces in their name
 -- TODO: support .tgz and similar extensions in addition to current .tar.*
--- TODO: support .rar as well
 
 local M = {}
 
@@ -23,6 +22,8 @@ local function get_common_prefix(archive, format)
         cmd = string.format('tar tf %q', archive)
     elseif format == 'zip' then
         cmd = string.format('zip --show-files %q', archive)
+    elseif format == 'rar' then
+        cmd = string.format('unrar vb %q', archive)
     else
         return nil, 'Unsupported format: '..format
     end
@@ -91,7 +92,7 @@ local function unpack(info)
     end
 
     local ext = vifm.fnamemodify(current, ':t:e')
-    if ext ~= 'zip' then
+    if ext ~= 'zip' and ext ~= 'rar' then
         ext = vifm.fnamemodify(current, ':t:r:e')
         if ext ~= 'tar' then
             vifm.sb.error('Unsupported file format')
@@ -140,6 +141,8 @@ local function unpack(info)
         cmd = string.format('tar -C %q -vxf %q', outdir, current)
     elseif ext == 'zip' then
         cmd = string.format('unzip -d %q %q', outdir, current)
+    elseif ext == 'rar' then
+        cmd = string.format('cd %q && unrar x %q', outdir, current)
     end
     local job = vifm.startjob { cmd = cmd }
 
diff --git a/data/plugins/viewcolumn/init.lua b/data/plugins/viewcolumn/init.lua
index 00d8ada..d14ab7f 100644
--- a/data/plugins/viewcolumn/init.lua
+++ b/data/plugins/viewcolumn/init.lua
@@ -11,6 +11,8 @@ Provides the following user-defined view column types:
  * AgeCtime -- relative age based on the ctime stat (11 characters in width)
  * AgeMtime -- relative age based on the mtime stat (11 characters in width)
 
+ * FileMtime -- modification time for files only
+
 Usage example:
 
     :set viewcolumns=-{NameLink},8.7{MCSize}
@@ -83,6 +85,14 @@ local function mcSize(info)
     end
 end
 
+local function fileMtime(info)
+    local text = ''
+    if not info.entry.isdir then
+        text = os.date(vifm.opts.global.timefmt, info.entry.mtime)
+    end
+    return { text = text }
+end
+
 local secsPerYear<const> = 365*24*60*60
 local function lsTime(info)
     local time = info.entry.mtime
@@ -135,6 +145,14 @@ if not added then
     vifm.sb.error("Failed to add MCSize view column")
 end
 
+local added = vifm.addcolumntype {
+    name = 'FileMtime',
+    handler = fileMtime
+}
+if not added then
+    vifm.sb.error("Failed to add FileMtime view column")
+end
+
 local time_units = {
     {name = 'year',   seconds = 365 * 24 * 60 * 60},
     {name = 'month',  seconds =  30 * 24 * 60 * 60},
diff --git a/data/shell-completion/bash/vifm b/data/shell-completion/bash/vifm
index 6bf637d..9c485f3 100644
--- a/data/shell-completion/bash/vifm
+++ b/data/shell-completion/bash/vifm
@@ -33,6 +33,10 @@ _vifm() {
             _filedir
             return
             ;;
+        --plugins-dir)
+            _filedir -d
+            return
+            ;;
         --server-name)
             local -a servers
             mapfile -t servers < <(command vifm --server-list 2>/dev/null)
@@ -49,7 +53,8 @@ _vifm() {
     if [[ $cur == -* ]]; then
         COMPREPLY=( $( compgen -W '--select -f --choose-files --choose-dir
             --delimiter --on-choose --logging= --server-list --server-name
-            --remote --remote-expr -c -h --help -v --version --no-configs' \
+            --remote --remote-expr -c -h --help -v --version --no-configs
+            --plugins-dir' \
             -- "$cur" ) )
         [[ $COMPREPLY == *= ]] && compopt -o nospace
         return
diff --git a/data/shell-completion/fish/vifm.fish b/data/shell-completion/fish/vifm.fish
index 2bd0ba9..b834379 100644
--- a/data/shell-completion/fish/vifm.fish
+++ b/data/shell-completion/fish/vifm.fish
@@ -13,8 +13,9 @@ end
 complete -c vifm    -F                            -a "(__fish_complete_hyphen)"
 complete -c vifm -r -F        -l "select"                                               -d "Open parent directory of the given path and select specified file in it"
 complete -c vifm    -f -s "f"                                                           -d "Make vifm instead of opening files write selection to \$VIFM/vifmfiles and quit"
-complete -c vifm -r -f        -l "choose-files"                                         -d "Set output file to write selection into on exit instead of opening files"
-complete -c vifm -r -f        -l "choose-dir"                                           -d "Set output file to write last visited directory into on exit"
+complete -c vifm -r -F        -l "choose-files"                                         -d "Set output file to write selection into on exit instead of opening files"
+complete -c vifm -r -F        -l "choose-dir"                                           -d "Set output file to write last visited directory into on exit"
+complete -c vifm -r -F        -l "plugins-dir"                                          -d "Additional plugins directory (can appear multiple times)"
 complete -c vifm -r -f        -l "delimiter"                                            -d "Set separator for list of file paths written out by vifm"
 complete -c vifm -r -f        -l "on-choose"      -a "(__fish_complete_subcommand)"     -d "Set command to be executed on selected files instead of opening them"
 complete -c vifm    -F        -l "logging"                                              -d "Log some operational details. If path is specified, early initialization is logged there"
diff --git a/data/shell-completion/zsh/_vifm b/data/shell-completion/zsh/_vifm
index be83a55..dcf1cfc 100644
--- a/data/shell-completion/zsh/_vifm
+++ b/data/shell-completion/zsh/_vifm
@@ -14,6 +14,7 @@ _arguments -C -s \
   '-f[makes vifm instead of opening files write selection to $VIFM/vimfiles and quit]' \
   '--choose-files[sets output file to write selection into on exit instead of opening files]:path or hyphen:_path_or_hyphen' \
   '--choose-dir[sets output file to write last visited directory into on exit]:path or hyphen:_path_or_hyphen' \
+  '--plugins-dir[adds additional plugins directory (can appear multiple times)]:dir:_directories' \
   '--delimiter[sets separator for list of file paths written out by vifm]:delimiter: ' \
   '--on-choose[sets command to be executed on selected files instead of opening them]:command:_cmdstring' \
   '--logging=-[log some operational details]::startup log path:_files' \
diff --git a/data/vifmrc b/data/vifmrc
index 0f3830d..bb72cb8 100644
--- a/data/vifmrc
+++ b/data/vifmrc
@@ -1,5 +1,5 @@
 " vim: filetype=vifm :
-" Sample configuration file for vifm (last updated: 8 August, 2022)
+" Sample configuration file for vifm (last updated: 3 April, 2023)
 " You can edit this file by hand.
 " The " character at the beginning of a line comments out the line.
 " Blank lines are ignored.
@@ -64,9 +64,6 @@ set vimhelp
 " press Enter, l or Right Arrow, set this.
 set norunexec
 
-" List of color schemes to try (picks the first one supported by the terminal)
-colorscheme Default-256 Default
-
 " Format for displaying time in file list. For example:
 " TIME_STAMP_FORMAT=%m/%d-%H:%M
 " See man date or man strftime for details.
@@ -104,6 +101,9 @@ endif
 " Set custom status line look
 set statusline="  Hint: %z%= %A %10u:%-7g %15s %20d  "
 
+" List of color schemes to try (picks the first one supported by the terminal)
+colorscheme Default-256 Default
+
 " ------------------------------------------------------------------------------
 " Bookmarks
 " ------------------------------------------------------------------------------
@@ -178,7 +178,7 @@ command! reload :write | restart full
 " program.  There is also %FOREGROUND, which is useful for entering passwords.
 
 " Pdf
-filextype {*.pdf},<application/pdf> zathura %c %i &, apvlv %c, xpdf %c
+filextype {*.pdf},<application/pdf> zathura %c %i, apvlv %c, xpdf %c
 fileviewer {*.pdf},<application/pdf> pdftotext -nopgbrk %c -
 
 " PostScript
@@ -186,7 +186,7 @@ filextype {*.ps,*.eps,*.ps.gz},<application/postscript>
         \ {View in zathura}
         \ zathura %f,
         \ {View in gv}
-        \ gv %c %i &,
+        \ gv %c %i,
 
 " Djvu
 filextype {*.djvu},<image/vnd.djvu>
@@ -195,23 +195,30 @@ filextype {*.djvu},<image/vnd.djvu>
         \ {View in apvlv}
         \ apvlv %f,
 
+" Midi
+filetype {*.mid,*.kar}
+       \ {Play using TiMidity++}
+       \ timidity %f,
+
 " Audio
-filetype {*.wav,*.mp3,*.flac,*.m4a,*.wma,*.ape,*.ac3,*.og[agx],*.spx,*.opus},
+filetype {*.wav,*.mp3,*.flac,*.m4a,*.wma,*.ape,*.ac3,*.og[agx],*.spx,*.opus,
+         \*.aac,*.mpga},
         \<audio/*>
-       \ {Play using ffplay}
-       \ ffplay -nodisp -hide_banner -autoexit %c,
        \ {Play using MPlayer}
-       \ mplayer %c,
+       \ mplayer %f,
        \ {Play using mpv}
-       \ mpv --no-video %c %s,
+       \ mpv --no-video %f %s,
+       \ {Play using ffplay}
+       \ ffplay -nodisp -hide_banner -autoexit %c,
 fileviewer {*.wav,*.mp3,*.flac,*.m4a,*.wma,*.ape,*.ac3,*.og[agx],*.spx,*.opus,
-           \*.aac}
+           \*.aac,*.mpga},
+          \<audio/*>
          \ ffprobe -hide_banner -pretty %c 2>&1
 
 " Video
 filextype {*.avi,*.mp4,*.wmv,*.dat,*.3gp,*.ogv,*.mkv,*.mpg,*.mpeg,*.vob,
           \*.fl[icv],*.m2v,*.mov,*.webm,*.ts,*.mts,*.m4v,*.r[am],*.qt,*.divx,
-          \*.as[fx]},
+          \*.as[fx],*.unknown_video},
          \<video/*>
         \ {View using ffplay}
         \ ffplay -fs -hide_banner -autoexit %f,
@@ -220,17 +227,17 @@ filextype {*.avi,*.mp4,*.wmv,*.dat,*.3gp,*.ogv,*.mkv,*.mpg,*.mpeg,*.vob,
         \ {View using mplayer}
         \ mplayer %f,
         \ {Play using mpv}
-        \ mpv --no-video %c %s,
+        \ mpv --no-video %f,
 fileviewer {*.avi,*.mp4,*.wmv,*.dat,*.3gp,*.ogv,*.mkv,*.mpg,*.mpeg,*.vob,
            \*.fl[icv],*.m2v,*.mov,*.webm,*.ts,*.mts,*.m4v,*.r[am],*.qt,*.divx,
-           \*.as[fx]},
+           \*.as[fx],*.unknown_video},
           \<video/*>
          \ ffprobe -hide_banner -pretty %c 2>&1
 
 " Web
 filextype {*.xhtml,*.html,*.htm},<text/html>
         \ {Open with qutebrowser}
-        \ qutebrowser %f %i &,
+        \ qutebrowser %f %i,
         \ {Open with firefox}
         \ firefox %f &,
 filetype {*.xhtml,*.html,*.htm},<text/html> links, lynx
@@ -243,6 +250,25 @@ filetype {*.[1-8]},<text/troff> man ./%c
 fileviewer {*.[1-8]},<text/troff> man ./%c | col -b
 
 " Images
+filextype {*.svg,*.svgz},<image/svg+xml>
+        \ {Edit in Inkscape}
+        \ inkscape %f,
+        \ {View in Inkview}
+        \ inkview %f,
+filextype {*.cr2}
+        \ {Open in Darktable}
+        \ darktable %f,
+        \ {Open in RawTherapee}
+        \ rawtherapee %f,
+filextype {*.xcf}
+        \ {Open in GIMP}
+        \ gimp %f,
+filextype {.kra}
+        \ {Open in Krita}
+        \ krita %f,
+filextype {.blend}
+        \ {Open in Blender}
+        \ blender %c,
 filextype {*.bmp,*.jpg,*.jpeg,*.png,*.gif,*.xpm},<image/*>
         \ {View in sxiv}
         \ sxiv %f,
@@ -290,7 +316,9 @@ filetype {*.asc},<application/pgp-signature>
 
 " Torrent
 filetype {*.torrent},<application/x-bittorrent> ktorrent %f &
-fileviewer {*.torrent},<application/x-bittorrent> dumptorrent -v %c
+fileviewer {*.torrent},<application/x-bittorrent>
+         \ dumptorrent -v %c,
+         \ transmission-show %c
 
 " FuseZipMount
 filetype {*.zip,*.jar,*.war,*.ear,*.oxt,*.apkg},
@@ -298,10 +326,10 @@ filetype {*.zip,*.jar,*.war,*.ear,*.oxt,*.apkg},
        \ {Mount with fuse-zip}
        \ FUSE_MOUNT|fuse-zip %SOURCE_FILE %DESTINATION_DIR,
        \ {View contents}
-       \ tar -tf %f | less,
+       \ unzip -l %f | less,
        \ {Extract here}
-       \ tar -vxf %c,
-fileviewer *.zip,*.jar,*.war,*.ear,*.oxt tar -tf %f
+       \ unzip %c,
+fileviewer *.zip,*.jar,*.war,*.ear,*.oxt unzip -l %f
 
 " ArchiveMount
 filetype {*.tar,*.tar.bz2,*.tbz2,*.tgz,*.tar.gz,*.tar.xz,*.txz,*.tar.zst,
diff --git a/data/vifmrc-osx b/data/vifmrc-osx
index bf03015..02ba54d 100644
--- a/data/vifmrc-osx
+++ b/data/vifmrc-osx
@@ -1,5 +1,5 @@
 " vim: filetype=vifm :
-" Sample configuration file for vifm on OSX (last updated: 8 August, 2022)
+" Sample configuration file for vifm on OSX (last updated: 2 April, 2023)
 " You can edit this file by hand.
 " The " character at the beginning of a line comments out the line.
 " Blank lines are ignored.
@@ -12,6 +12,10 @@
 " Command used to edit files in various contexts.  The default is vim.
 " If you would like to use another vi clone such as Elvis or Vile
 " you will need to change this setting.
+"
+" Mind that due to `filetype * open` below by default the editor won't be used
+" for opening files via l/Enter keys.  Comment that line out to change the
+" behaviour.
 if executable('vim')
     set vicmd=vim
 elseif executable('nvim')
@@ -64,9 +68,6 @@ set vimhelp
 " press Enter, l or Right Arrow, set this.
 set norunexec
 
-" List of color schemes to try (picks the first one supported by the terminal)
-colorscheme Default-256 Default
-
 " Format for displaying time in file list. For example:
 " TIME_STAMP_FORMAT=%m/%d-%H:%M
 " See man date or man strftime for details.
@@ -102,6 +103,9 @@ set slowfs=curlftpfs
 " Set custom status line look
 set statusline="  Hint: %z%= %A %10u:%-7g %15s %20d  "
 
+" List of color schemes to try (picks the first one supported by the terminal)
+colorscheme Default-256 Default
+
 " ------------------------------------------------------------------------------
 " Bookmarks
 " ------------------------------------------------------------------------------
@@ -190,7 +194,7 @@ filetype {*.djvu},<image/vnd.djvu> open -a MacDjView.app
 
 " Audio
 filetype {*.wav,*.mp3,*.flac,*.m4a,*.wma,*.ape,*.ac3,*.og[agx],*.spx,*.opus,
-         \*.aac},
+         \*.aac,*.mpga},
         \<audio/*>
        \ {Open in Music}
        \ open -a Music.app,
@@ -199,13 +203,14 @@ filetype {*.wav,*.mp3,*.flac,*.m4a,*.wma,*.ape,*.ac3,*.og[agx],*.spx,*.opus,
        \ {Open in IINA}
        \ open -a IINA.app,
 fileviewer {*.wav,*.mp3,*.flac,*.m4a,*.wma,*.ape,*.ac3,*.og[agx],*.spx,*.opus,
-           \*.aac}
+           \*.aac,*.mpga},
+          \<audio/*>
          \ ffprobe -hide_banner -pretty %c 2>&1
 
 " Video
 filetype {*.avi,*.mp4,*.wmv,*.dat,*.3gp,*.ogv,*.mkv,*.mpg,*.mpeg,*.vob,
          \*.fl[icv],*.m2v,*.mov,*.webm,*.ts,*.mts,*.m4v,*.r[am],*.qt,*.divx,
-         \*.as[fx]},
+         \*.as[fx],*.unknown_video},
         \<video/*>
        \ {Open in QuickTime Player}
        \ open -a QuickTime\ Player.app,
@@ -215,7 +220,7 @@ filetype {*.avi,*.mp4,*.wmv,*.dat,*.3gp,*.ogv,*.mkv,*.mpg,*.mpeg,*.vob,
        \ open -a VLC.app,
 fileviewer {*.avi,*.mp4,*.wmv,*.dat,*.3gp,*.ogv,*.mkv,*.mpg,*.mpeg,*.vob,
            \*.fl[icv],*.m2v,*.mov,*.webm,*.ts,*.mts,*.m4v,*.r[am],*.qt,*.divx,
-           \*.as[fx]},
+           \*.as[fx],*.unknown_video},
           \<video/*>
          \ ffprobe -hide_banner -pretty %c 2>&1
 
@@ -266,7 +271,9 @@ filetype *.sha512
 
 " Torrent
 filetype {*.torrent},<application/x-bittorrent> open -a Transmission.app
-fileviewer {*.torrent},<application/x-bittorrent> dumptorrent -v %c
+fileviewer {*.torrent},<application/x-bittorrent>
+         \ dumptorrent -v %c,
+         \ transmission-show %c
 
 " Extract zip files
 filetype {*.zip},<application/zip,application/java-archive>
@@ -335,10 +342,8 @@ fileviewer {*.docx},
 
 " Open all other files with default system programs (you can also remove all
 " :file[x]type commands above to ensure they don't interfere with system-wide
-" settings).  By default all unknown files are opened with 'vi[x]cmd'
-" uncommenting one of lines below will result in ignoring 'vi[x]cmd' option
-" for unknown file types.
-" For OS X:
+" settings).  Use of the line below results in ignoring 'vi[x]cmd' option for
+" unknown file types on l/Enter keys.
 filetype * open
 
 " ------------------------------------------------------------------------------
diff --git a/data/vim/doc/app/vifm-app.txt b/data/vim/doc/app/vifm-app.txt
index 9a4497f..24f75b8 100644
--- a/data/vim/doc/app/vifm-app.txt
+++ b/data/vim/doc/app/vifm-app.txt
@@ -1,4 +1,4 @@
-*vifm-app.txt*    For Vifm version 0.12.1  Last change: 2022 Sep 21
+*vifm-app.txt*    For Vifm version 0.13  Last change: 2023 Apr 04
 
  Email for bugs and suggestions: <xaizek@posteo.net>
 
@@ -35,6 +35,7 @@
 |vifm-trash|              Details about trash directory in vifm.
 |vifm-clientserver|       Client-server communication.
 |vifm-ext-rename|         About editing buffer of file names in an editor.
+|vifm-mouse-using|        Using the mouse.
 |vifm-plugin|             Using the vifm.vim plugin.
 |vifm-reserved|           List of reserved commands.
 |vifm-env-vars|           Environment variables that affect vifm or set by it.
@@ -49,6 +50,7 @@ Tag name structure:
       Menu or dialog command   vifm-m_     :help vifm-m_zh
       Command-line command     vifm-:      :help vifm-:quit
       Command-line editing     vifm-c_     :help vifm-c_CTRL-H
+      Command-line navigation  vifm-a_     :help vifm-a_CTRL-Y
       Vifm command argument    vifm--      :help vifm--f
       Option                   vifm-'      :help vifm-'wrap'
 
@@ -100,6 +102,9 @@ The other command line arguments are:
     sets command to be executed on selected files instead of opening them.
     The command may use any of |vifm-macros|.  The command is executed once for
     whole selection.
+--plugins-dir <path>                           *vifm---plugins-dir*
+    additional plugins directory (can appear multiple times).  The last one
+    added has the highest priority.
 --logging[=<startup log path>]                 *vifm---logging*
     log some operational details to $XDG_DATA_HOME/vifm/log or $VIFM/log.  If
     the optional startup log path is specified and permissions allow to open
@@ -228,8 +233,10 @@ zt - redraw pane with file in top of list.     *vifm-zt*
 zz - redraw pane with file in center of list.  *vifm-zz*
 zb - redraw pane with file in bottom of list.  *vifm-zb*
 
-Ctrl-E - scroll pane one line down.            *vifm-CTRL-E*
-Ctrl-Y - scroll pane one line up.              *vifm-CTRL-Y*
+Ctrl-E                                         *vifm-CTRL-E*
+    scroll pane one line down or column right (in transposed ls-like view).
+Ctrl-Y                                         *vifm-CTRL-Y*
+    scroll pane one line up or column left (in transposed ls-like view).
 
 Pane manipulation~
 
@@ -610,14 +617,20 @@ u - undo last change.                          *vifm-u*
 Ctrl-R - redo last change.                     *vifm-CTRL-R*
 
 dp                                             *vifm-dp*
-    in compare view of "ofboth grouppaths" kind, makes corresponding entry
-    of the other pane equal to the current one.  The semantics is as follows:
-     - nothing done for identical entries
+    in compare view of "ofboth grouppaths" kind makes corresponding entries
+    of the other pane equal to the current one.  If at least one file is
+    selected, the command processes selection, otherwise current file.
+
+    The semantics is as follows:
+     - nothing is done for identical entries
      - if file is missing in current view, its pair gets removed
      - if file is missing or differs in other view, it is replaced
+     - file pairs are defined by matching relative paths
+
     File removal obeys |vifm-'trash'| option.  When the option is enabled, the
     operation can be undone/redone (although results won't be visible
     automatically).
+
     Unlike in Vim, this operation is performed on a single line rather than
     a set of adjacent changes.
 do                                             *vifm-do*
@@ -1027,6 +1040,55 @@ Ctrl-R =                                       *vifm-c_CTRL-R_=*
     via nested command-line prompt (where this key does nothing).  Expansion
     of an erroneous expression is empty.
 
+Fast navigation~
+
+In order to streamline navigation through directory tree, you can enter a
+special form of command-line mode from search or local filter prompt.  Once
+activated, pressing Enter opens currently selected directory and clears the
+prompt in anticipation of the next component of the path.  If entry under the
+cursor is a file, it is opened and the mode is finished.
+
+This behaviour is embedded in a command-line mode, but doesn't update input
+histories nor expands abbreviations and redefines some of the mode's mappings
+for the purpose of faster navigation through the file system rather than
+command-line editing.  When on, prompt gets "nav" prefix.
+
+You can enable this behaviour on search by default via a mapping like: >
+    nnoremap / /<c-y>
+
+Ctrl-Y                                         *vifm-c_CTRL-Y*
+    enter navigation mode.  Works only for search and local filter started
+    from a normal mode and only when |vifm-'incsearch'| is set
+    (|vifm-'wrapscan'| is also nice to have set for search).
+
+Ctrl-Y                                         *vifm-a_CTRL-Y*
+    return to a regular command-line mode.
+
+Enter, Right                                   *vifm-a_Enter* *vifm-a_Right*
+    either enter a directory under the cursor without leaving the mode and
+    clear the prompt or leave the mode for files.  If |vifm-'navoptions'|
+    specifies "open:all" a file under the cursor is opened after leaving the
+    mode.
+Ctrl-O, Left                                   *vifm-a_CTRL-O* *vifm-a_Left*
+    go to parent directory.
+Ctrl-J                                         *vifm-a_CTRL-J*
+    leave the mode without undoing cursor position or filter state.
+
+Ctrl-N, Down                                   *vifm-a_CTRL-N* *vifm-a_Down*
+    move view cursor down.
+Ctrl-P, Up                                     *vifm-a_CTRL-P* *vifm-a_Up*
+    move view cursor up.
+
+Page Down                                      *vifm-a_PageDown*
+    scroll view down.
+Page Up                                        *vifm-a_PageUp*
+    scroll view up.
+
+Home                                           *vifm-a_Home*
+    move view cursor to the first item.
+End                                            *vifm-a_End*
+    move view cursor to the last item.
+
 Pasting special values~
 
 The shortcuts listed below insert specified values into current cursor
@@ -1184,6 +1246,8 @@ line.  If you want to use '|' in an argument, precede it with '\'.
 These commands see '|' as part of their arguments even when it's escaped:
 
     :[range]!
+    :amap
+    :anoremap
     :autocmd
     :cabbrev
     :cmap
@@ -1500,12 +1564,22 @@ The builtin commands are:
       :%copy
 <
                                                *vifm-:compare*
-:compare [byname | bysize | bycontents | listall | listunique | listdups |
-          ofboth | ofone | groupids | grouppaths | skipempty]...
+:compare [byname | bysize | bycontents |
+          listall | listunique | listdups |
+          ofboth | ofone |
+          groupids | grouppaths |
+          skipempty | withicase | withrcase |
+          showidentical | showdifferent | showuniqueleft | showuniqueright]...
     compare files in one or two views according to the arguments.  The default
-    is "bycontents listall ofboth grouppaths".  See |vifm-compare-views| for
+    is "bycontents listall ofboth grouppaths showidentical showdifferent
+    showuniqueleft showuniqueright".  See |vifm-compare-views| for
     details.  Diff structure is incompatible with alternative representations,
     so values of |vifm-'lsview'| and |vifm-'millerview'| options are ignored.
+:compare! (showidentical | showdifferent | showuniqueleft |
+           showuniqueright)...
+    this invocation form works only when compare view is active and results in
+    redoing of the previous :compare with toggled state of the passed in
+    options.
 
 :cope[n]                                       *vifm-:copen* *vifm-:cope*
     reopens the last visible menu that has navigation to files by default, if
@@ -1870,7 +1944,8 @@ Available group-name values:
  - CmpMismatch - color of mismatched files in side-by-side comparison by paths
  - CmpUnmatched - comparison file entry that has no pair in the other pane
  - CmpBlank - entry placeholder in a compare view, paired with CmpUnmatched
- - User1..User9 - 9 colors which can be used via %* |vifm-'statusline'| macro
+ - User1..User20 - 20 colors which can be used via %* |vifm-'statusline'|
+   macro
 
 Available colors:
  - -1 or default or none - transparent
@@ -2016,7 +2091,7 @@ transparency:
   SuggestBox
   StatusLine
     WildMenu
-    User1..User9
+    User1..User20
   Border
   CmdLine
     ErrorMsg
@@ -2044,10 +2119,10 @@ transparency:
   TopLine
     TopLineSel
       TabLineSel (for pane tabs)
-        User1..User9
+        User1..User20
   TabLine
     TabLineSel
-      User1..User9
+      User1..User20
 
 "none" means default terminal color for highlight groups at the first level
 of the hierarchy and transparency for all others.
@@ -2259,6 +2334,14 @@ order using the '.' operator.  Any whitespace is ignored.
 :redr[aw]                                      *vifm-:redraw* *vifm-:redr*
     redraw the screen immediately.
 
+                                               *vifm-:regedit* *vifm-:rege*
+:rege[dit] [{reg}]
+    edit register contents using external editor (see |vifm-'vicmd'|).
+    If {reg} is omitted, unnamed register will be edited by default.
+    Edited paths are normalized (no extra `.`, `..`, `/`, etc.) and all
+    relative paths are treated as starting in the directory of the current
+    view.
+
                                                *vifm-:registers* *vifm-:reg*
 :reg[isters]
     display menu with registers content.
@@ -2655,12 +2738,14 @@ yet supported) and might be changed in future releases, or get an alias.
 :map! lhs rhs
     map lhs key sequence to rhs in command line mode.
 
+                                               *vifm-:amap*
                                                *vifm-:cmap* *vifm-:cm*
                                                *vifm-:dmap* *vifm-:dm*
                                                *vifm-:mmap* *vifm-:mm*
                                                *vifm-:nmap* *vifm-:nm*
                                                *vifm-:qmap* *vifm-:qm*
                                                *vifm-:vmap* *vifm-:vm*
+:amap   lhs rhs - map lhs to rhs in navigation mode.
 :cm[ap] lhs rhs - map lhs to rhs in command line mode.
 :dm[ap] lhs rhs - map lhs to rhs in dialog modes.
 :mm[ap] lhs rhs - map lhs to rhs in menu mode.
@@ -2668,6 +2753,7 @@ yet supported) and might be changed in future releases, or get an alias.
 :qm[ap] lhs rhs - map lhs to rhs in view mode.
 :vm[ap] lhs rhs - map lhs to rhs in visual mode.
 
+:amap   - list all maps of navigation mode.
 :cm[ap] - list all maps of command line mode.
 :dm[ap] - list all maps of dialog modes.
 :mm[ap] - list all maps of menu mode.
@@ -2675,6 +2761,8 @@ yet supported) and might be changed in future releases, or get an alias.
 :qm[ap] - list all maps of view mode.
 :vm[ap] - list all maps of visual mode.
 
+:amap beginning
+    list all maps of navigation mode that start with the beginning.
 :cm[ap] beginning
     list all maps of command line mode that start with the beginning.
 :dm[ap] beginning
@@ -2696,12 +2784,16 @@ yet supported) and might be changed in future releases, or get an alias.
     map the key sequence lhs to rhs for command line mode, but don't expand
     user mappings in rhs.
 
+                                               *vifm-:anoremap*
                                                *vifm-:cnoremap* *vifm-:cno*
                                                *vifm-:dnoremap* *vifm-:dn*
                                                *vifm-:mnoremap* *vifm-:mn*
                                                *vifm-:nnoremap* *vifm-:nn*
                                                *vifm-:qnoremap* *vifm-:qn*
                                                *vifm-:vnoremap* *vifm-:vn*
+:anoremap lhs rhs
+    map the key sequence lhs to rhs for navigation mode, but don't expand
+    user mappings in rhs.
 :cno[remap] lhs rhs
     map the key sequence lhs to rhs for command line mode, but don't expand
     user mappings in rhs.
@@ -2727,12 +2819,14 @@ yet supported) and might be changed in future releases, or get an alias.
 :unm[ap]! lhs
     remove user mapping of lhs from command line mode.
 
+                                               *vifm-:aunmap*
                                                *vifm-:cunmap* *vifm-:cu*
                                                *vifm-:dunmap* *vifm-:du*
                                                *vifm-:munmap* *vifm-:mu*
                                                *vifm-:nunmap* *vifm-:nun*
                                                *vifm-:qunmap* *vifm-:qun*
                                                *vifm-:vunmap* *vifm-:vu*
+:aunmap   lhs - remove user mapping of lhs from navigation mode.
 :cu[nmap] lhs - remove user mapping of lhs from command line mode.
 :du[nmap] lhs - remove user mapping of lhs from dialog modes.
 :mu[nmap] lhs - remove user mapping of lhs from menu mode.
@@ -2838,12 +2932,12 @@ The command macros may be used in user commands.
                                                                *vifm-%n*
   %n        forbid use of terminal multiplexer to run the command.
                                                                *vifm-%N*
-  %N        do not detach viewer from terminal session (leaves `/dev/tty`
-            available).
+  %N        do not detach viewer from terminal session (keeps `/dev/tty`
+            available) or process group (keeps the command in the set of
+            foreground clients of the terminal).
                                                                *vifm-%i*
-  %i        completely ignore command output.  For background jobs this
-            suppresses error dialogs, while still storing errors
-            internally for viewing via |vifm-:jobs| menu.
+  %i        run in background and suppress error dialogs, but collect
+            errors internally for viewing via |vifm-:jobs| menu.
 
                                                                *vifm-%Pl*
   %Pl       pipe list of files to standard input of a command.
@@ -3672,6 +3766,7 @@ scope: local
 Configures ls-like view.
 
     item          used for ~
+    columncount   fixed number of columns to display or 0
     transposed    filling view grid by columns rather than by lines
 
                                                *vifm-'lsview'*
@@ -3703,8 +3798,8 @@ Configures miller view.
 column, but central (main) column can't be disabled.
 
 rpreview specifies what file-system objects should be previewed in the right
-column and can take two values: dirs (only directories) or all.  Both options
-don't include parent directory ("..").
+column and takes three values: dirs (only directories), files (only files) or
+all.  Neither value enables preview of parent directory ("..").
 
 Example of two-column mode which is useful in combination with
 |vifm-:view| command: >
@@ -3730,6 +3825,32 @@ operations (detecting changes made by external applications, monitoring
 background jobs, redrawing UI).  There are no strict guarantees, however the
 higher this value is, the less is CPU load in idle mode.
 
+                                               *vifm-'mouse'*
+mouse
+type: charset
+default: ""
+
+Contains a sequence of single-character flags:
+ - a - all supported modes (a shorthand for all the rest and future additions)
+ - c - command-line mode (includes navigation mode)
+ - m - menu mode
+ - n - normal mode
+ - q - view mode
+ - v - visual mode
+
+                                               *vifm-'navoptions'*
+navoptions
+type: string list
+default: "open:dirs"
+
+Configures behaviour of navigation mode.
+
+    item      default~
+    open:str  dirs
+
+The "open" item specifies what file-system objects should be opened on
+|vifm-a_Enter| and can take two values: dirs (only directories) or all.
+
                                                *vifm-'number'* *vifm-'nu'*
 number nu
 type: boolean
@@ -3850,7 +3971,7 @@ type: boolean
 default: false
 
 Run executable file on Enter, l or Right Arrow key.  Behaviour of the last two
-depends on the value of the 'lsview' option.
+depends on the value of the |vifm-'lsview'| option.
 
                                                *vifm-'scrollbind'* *vifm-'scb'*
 scrollbind scb
@@ -4113,9 +4234,9 @@ are supported:
     %c - size of current FS
     %z - short tips/tricks/hints that chosen randomly after one minute period
     `%{<expr>}` - evaluate arbitrary vifm expression `<expr>`, e.g. `&sort`
-    %* - resets or applies one of User1..User9 highlight groups; reset happens
-         when width field is 0 or not specified, one of groups gets picked
-         when width field is in the range from 1 to 9
+    %* - resets or applies one of User1..User20 highlight groups; reset happens
+         when width field is 0 or not specified, one of the groups gets picked
+         when width field is in the range from 1 to 20
     all |vifm-'rulerformat'| macros
 Percent sign can be followed by optional minimum field width.  Add '-' before
 minimum field width if you want field to be right aligned.
@@ -4186,30 +4307,31 @@ tablabel
 type: string
 default: ""
 
-When non-empty, determines format of the main part of a single tab's label.
+When non-empty and |vifm-'tabline'| isn't set, determines format of the main
+part of a single tab's label.
 
 When empty, tab label is set to either tab name for named tabs or to view
 title (usually current path) for unnamed tabs.
 
 The following macros can appear in the format (see below for what a flag is):
-    %C      - flag of a current tab
-    %N      - number of the tab
-    %T      - flag of a tree mode
-    %c      - description of a custom view
-    %n      - name of the tab
-    %p      - path of the view (handles |vifm-filename-modifiers|)
-    %t      - title of the view (affected by |vifm-'shortmess'| flags)
-    %%      - literal percent sign
-    %[      - designates beginning of an optional block
-    %]      - designates end of an optional block
-    %*, %0* - resets highlighting
-    %1-%9   - applies one of User1..User9 highlight groups
+    %C         - flag of a current tab
+    %N         - number of the tab
+    %T         - flag of a tree mode
+    %c         - description of a custom view
+    %n         - name of the tab
+    %p         - path of the view (handles |vifm-filename-modifiers|)
+    %t         - title of the view (affected by |vifm-'shortmess'| flags)
+    %%         - literal percent sign
+    %[         - designates beginning of an optional block
+    %]         - designates end of an optional block
+    %*, %0*    - resets highlighting
+    %1* - %20* - applies one of User1..User20 highlight groups
 
 In global tabs the view in bullets above refers to currently active view of that
 tab.
 
 Flag macros are a special kind of macros that always expand to an empty value
-and are ment to be used inside optional blocks to control their visibility.
+and are meant to be used inside optional blocks to control their visibility.
 
 Optional blocks are ignored unless at least one macro inside of them is
 expanded to a non-empty value or is a set flag macro.
@@ -4224,6 +4346,18 @@ Example: >
  " %p:t            -- tail part of view's location
  set tablabel=%[(%n)%]%[%[%T{tree}%]%[{%c}%]@%]%p:t
 <
+                                               *vifm-'tabline'* *vifm-'tal'*
+tabline tal
+type: string
+default: ""
+
+When non-empty, determines format of the tab line.  Note that mouse clicks
+won't be handled when this option is non-empty.
+
+The following macros can appear in the format:
+    %*, %0*    - resets highlighting
+    %1* - %20* - applies one of User1..User20 highlight groups
+
                                                *vifm-'tabprefix'*
 tabprefix
 type: string
@@ -4507,6 +4641,19 @@ Searches wrap around end of the list.
 --------------------------------------------------------------------------------
 *vifm-mappings*
 
+A user mapping like `nnoremap lhs rhs` defines a substitution of the
+left-hand-side (LHS) with the right-hand-side (RHS) in the input stream.  A
+regular mapping (without "nore" in :command's name) expands recognized
+sequences in the RHS, while "*noremap" mapping always interprets RHS as if no
+user mappings were defined and each key has its builtin meaning.  In most
+cases you want to use noremap variant and if your RHS includes LHS, only
+noremap variant will work because recursion in a mapping is not allowed.
+
+In order to define a mapping determine in which mode you want to activate it
+and use an appropriate "*noremap" :command (e.g., |vifm-:nnoremap| for a
+normal mode mapping).  RHS doesn't have to limit itself to the mode in which
+the mapping was started and can span multiple modes.
+
 Map arguments~
 
 LHS of mappings can be preceded by arguments which take the form of special
@@ -4878,11 +5025,15 @@ expand({expr})        String      Expands special keywords in {expr}.
 extcached({cache}, {path}, {extcmd})
                       String      Caches output of {extcmd} per {cache} and
                                   {path} combination.
+filereadable({path})  Integer     Checks whether {expr} points to a
+                                  non-directory that can be read.
 filetype({fnum} [, {resolve}])
                       String      Returns file type from position.
 fnameescape({expr})   String      Escapes {expr} for use in a :command.
 getpanetype()         String      Returns type of current pane.
 has({property})       Integer     Checks whether instance has {property}.
+input({prompt} [, {initial} [, {completion}]])
+                      String      Prompts user for an input on command-line.
 layoutis({type})      Integer     Checks whether layout is of type {type}.
 paneisat({loc})       Integer     Checks whether current pane is at {loc}.
 system({command})     String      Executes shell command and returns its output.
@@ -4915,7 +5066,6 @@ Example: >
           fileview * defviewer %c
       endif
   endif
-<
 
 expand({expr})                                 *vifm-expand()*
 
@@ -4947,6 +5097,12 @@ Example: >
                                      \ expand('%c'),
                                      \ expand('stat --format=%%bx%%B %c')) }"
 
+filereadable({path})                           *vifm-filereadable()*
+
+Checks whether {path} exists and refers to a non-directory entry and its
+permissions allow reading.  Returns boolean value describing result of the
+check.
+
 filetype({fnum} [, {resolve}])                 *vifm-filetype()*
 
 The result is a string, which represents file type and is one of the list:
@@ -4987,6 +5143,7 @@ Retrieves string describing type of current pane.  Possible return values:
     custom       custom file list (%u)
     very-custom  very custom file list (%U)
     tree         tree view
+    compare      compare view
 
 
 has({property})                                *vifm-has()*
@@ -5008,6 +5165,27 @@ Usage example: >
   execute 'set' 'statusline="  %t%= %A '.$RIGHTS.'%15E %20d  "'
 <
 
+input({prompt} [, {initial} [, {completion}]]) *vifm-input()*
+
+Creates a command-line prompt to obtain user's input.  Initial value can
+be supplied as an optional second parameter, otherwise empty string is used.
+
+Optional third parameter specifies kind of completion, which can be one of:
+    dir   paths to directories
+    file  paths to files and directories
+    ""    (empty string, default) no completion
+
+Note that behaviour differs from Vim where executing a mapping like >
+  nnoremap j :echo input('text: ')<cr>input
+leaves you in a prompt mode with "input" typed in.  Vifm will wait for leaving
+the prompt and then continue executing the mapping.
+
+Usage example: >
+  nnoremap ,m : let $DIR_NAME = input('mkdir: ', '', 'dir')
+             \| if $DIR_NAME != ''
+             \|     execute 'mkdir' fnameescape($DIR_NAME)
+             \| endif<cr>
+
 layoutis({type})                               *vifm-layoutis()*
 
 Checks whether current interface layout is {type} or not, where {type} can
@@ -5288,17 +5466,19 @@ d - (only for execute flags) means u-x+X, g-x+X or o-x+X argument for the
     bit from all files, but preserve it for directories, set all execute flags
     to 'd' and check 'Set Recursively' flag.
 
-Jobs menu~
+Jobs menu (:jobs)~
 
-dd requests cancellation of job under cursor.  The job won't be removed from
+dd - request cancellation of job under cursor.  The job won't be removed from
 the list, but marked as being cancelled (if cancellation was successfully
 requested).  A message will pop up if the job has already stopped.
 Note that on Windows cancelling external programs like this might not work,
 because their parent shell doesn't have any windows.
 
-e key displays errors of selected job if any were collected.  They are
+e - display errors of selected job if any were collected.  They are
 displayed in a new menu, but you can get back to jobs menu by pressing h.
 
+r - reload the list of jobs.
+
 Undolist menu~
 
 r - reset undo position to group under the cursor.
@@ -5432,7 +5612,7 @@ grouping to preserve as all file ids are guaranteed to be distinct.
 
 Creation~
 
-Arguments passed to |vifm-:compare| form four categories each with its own
+Arguments passed to |vifm-:compare| form seven categories each with its own
 prefix and is responsible for particular property of operation.
 
 Which files to compare:
@@ -5459,9 +5639,22 @@ How results are grouped (has no effect if "ofone" specified):
 Which files to omit:
  - skipempty - ignore empty files.
 
+Comparison tweaks:
+ - withicase - ignore case when comparing file names/paths;
+ - withrcase - respect case when comparing file names/paths.
+
+Which results to show (has no effect for single pane comparison):
+ - showidentical   - toggle showing of identical files;
+ - showdifferent   - toggle showing of different files;
+ - showuniqueleft  - toggle showing of unique top/left files;
+ - showuniqueright - toggle showing of unique bottom/right files.
+
 Each argument can appear multiple times, the rightmost one of the group is
 considered.  Arguments alter default behaviour instead of substituting it.
 
+When neither `withicase` nor `withrcase` is specified, case depends on the
+running operating system and the file system on which the files are located.
+
 Examples~
 
 The defaults corresponds to probably the most common use case of comparing
@@ -5470,6 +5663,7 @@ files in two trees with grouping by paths, so the following are equivalent: >
  :compare
  :compare bycontents grouppaths
  :compare bycontents listall ofboth grouppaths
+ :compare showidentical showdifferent showuniqueleft showuniqueright
 
 Another use case is to find duplicates in the current sub-tree: >
 
@@ -5487,6 +5681,9 @@ data.
 Comparison views have second column displaying id of the file, files with same
 id are considered to be equal.  The view columns configuration is predefined.
 
+The status bar displays only the initial result of the comparison and can be
+out of date.
+
 Behaviour~
 
 When two views are being compared against each other the following changes to
@@ -6024,6 +6221,78 @@ vertical split.
 You can cancel renaming by removing all non-comments from the buffer.  This
 also erases information about previous edits.
 
+--------------------------------------------------------------------------------
+*vifm-mouse-using*
+
+Note: <ScrollWheelDown> is not available on 32-bit *nix systems, because
+ncurses doesn't support it there (limitation of implementation).
+
+Note: these are not available in mappings at the momemnt.
+
+                                               *vifm-mouse-overview*
+Normal Mode:
+event             position  change  action        ~
+                   cursor   window                ~
+<LeftMouse>         yes      yes    <cr> if cursor wasn't moved
+<LeftRelease>        no      yes
+<MiddleMouse>        no      yes    <c-e>
+<MiddleRelease>      no      yes
+<RightMouse>        yes      yes    |vifm-:file|
+<RightRelease>       no      yes
+<ScrollWheelUp>      no      yes    <c-y> or |vifm-:tabprevious|
+<ScrollWheelDown>    no      yes    <c-e> or |vifm-:tabnext|
+
+Clicking on or scrolling over an inactive pane (including its title), makes it
+active and does nothing else.  Tabs are scrolled when mouse hovers over them.
+
+Clicking on the left miller column goes to parent directory and clicking the
+right one opens current entry.
+
+Visual Mode:
+event             position  selection  action     ~
+                   cursor                         ~
+<LeftMouse>         yes      update    <cr> if cursor wasn't moved
+<LeftRelease>        no
+<MiddleMouse>        no      update    <c-e>
+<MiddleRelease>      no
+<RightMouse>         no
+<RightRelease>       no
+<ScrollWheelUp>      no      update    <c-y>
+<ScrollWheelDown>    no      update    <c-e>
+
+Command-line Mode:
+event             position  action                ~
+                   cursor                         ~
+<LeftMouse>         yes
+<LeftRelease>        no
+<MiddleMouse>        no     <c-n>
+<MiddleRelease>      no
+<RightMouse>         no
+<RightRelease>       no
+<ScrollWheelUp>      no     <c-p>
+<ScrollWheelDown>    no     <c-n>
+
+Menu Mode:
+event             position  action                ~
+                   cursor                         ~
+<LeftMouse>         yes     <cr> if cursor wasn't moved
+<LeftRelease>        no
+<MiddleMouse>        no     <c-e>
+<MiddleRelease>      no
+<RightMouse>         no
+<RightRelease>       no
+<ScrollWheelUp>      no     <c-y>
+<ScrollWheelDown>    no     <c-e>
+
+View Mode:
+event               action                ~
+                                          ~
+<ScrollWheelUp>     k
+<ScrollWheelDown>   j
+
+Clicking on or scrolling over an inactive pane (including its title), detaches
+view mode if it wasn't activated for exploring a file.
+
 --------------------------------------------------------------------------------
 *vifm-plugin*
 
diff --git a/data/vim/doc/app/vifm-lua.txt b/data/vim/doc/app/vifm-lua.txt
index e60619b..5706e1b 100644
--- a/data/vim/doc/app/vifm-lua.txt
+++ b/data/vim/doc/app/vifm-lua.txt
@@ -1,10 +1,12 @@
-*vifm-lua.txt*    For Vifm version 1.0  Last change: 2022 Sep 17
+*vifm-lua.txt*    For Vifm version 1.0  Last change: 2023 Mar 19
 
  Email for bugs and suggestions: <xaizek@posteo.net>
 
 Note: this is very much work in progress.  Everything can change up to
 complete removal of this interface (that's an unlikely scenario though).
 
+Current API version: v0.1.0
+
 |vifm-lua-status|      Status of plugins.
 |vifm-lua-plugins|     Sample plugins.
 |vifm-lua-lua|         Generic reasoning behind this API.
@@ -13,10 +15,12 @@ complete removal of this interface (that's an unlikely scenario though).
 |vifm-lua-loading|     Which plugins get loaded.
 |vifm-lua-libs|        What Lua libraries are available.
 |vifm-lua-handlers|    Lua handlers concept.
+|vifm-lua-events|      Description of event types.
 |vifm-lua-caps|        What using Lua in Vifm buys you.
 |vifm-lua-api|         Root-level API.
 |vifm-l_vifm|          `vifm` global table.
 |vifm-l_vifm.cmds|     `vifm.cmds` global table.
+|vifm-l_vifm.events|   `vifm.events` global table.
 |vifm-l_vifm.keys|     `vifm.keys` global table.
 |vifm-l_vifm.opts|     `vifm.opts` global table.
 |vifm-l_vifm.plugin|   `vifm.plugin` global table.
@@ -97,7 +101,11 @@ Safe ones can be called from anywhere, but unsafe API is not always
 accessible.  As an example, you can't change directory of any view or add a
 view column from a view column handler.
 
-Unsafe API is marked with {unsafe} in its description.
+Unsafe API is marked with {unsafe} in its description.  If description of a
+handler doesn't say that it's run in a safe environment, it can use unsafe API
+as well.  In order to allow use of unsafe API, an execution of a handler might
+be postponed until Vifm reaches a state where it holds the least amount of
+assumptions about what's going on, such handlers are marked as {delayed}.
 
 --------------------------------------------------------------------------------
 *vifm-lua-evolution*
@@ -116,7 +124,7 @@ welcome.
 Current design principles:
  1. Callbacks in Lua accept a table and return one (if they return anything).
     This allows adding new fields to both input and output while staying
-    backward compatible and readable.
+    backward compatible and readable at the cost of being a bit more verbose.
 
 Random ideas~
 
@@ -164,9 +172,24 @@ The following standard libraries are enabled:
 --------------------------------------------------------------------------------
 *vifm-plugins*
 
-After processing contents of |vifm-vifmrc| $VIFM/plugins/ directory is
-enumerated in search of plugins.  Directories or symbolic links to directories
-are considered as candidates.  Those starting with a dot are ignored.
+Loading of plugins is performed after processing contents of |vifm-vifmrc|.
+Plugins are searched in the directories specified via |vifm---plugins-dir|
+command-line option and the last specified directory is searched first.  Then
+$VIFM/plugins/ is enumerated in search of plugins.  Directories or symbolic
+links to directories are considered as candidates.  Those starting with a dot
+are ignored.
+
+For example, running Vifm like >
+
+  vifm --plugins-dir dirA --plugins-dir ~/dirB
+
+results in search for plugins in this order of directories:
+ 1. $PWD/dirA
+ 2. ~/dirB
+ 3. $VIFM/plugins
+The order matters as it defines which of the conflicting plugins gets
+loaded (see below on name conflicts).  Such an ordering allows shadowing
+plugins from default location.
 
 Implications of the default order of initialization:
  * :commands defined in configuration have precedence over :commands defined by
@@ -181,7 +204,9 @@ All plugins are required to contain `init.lua` file at their root.  This file
 gets processed and its return value is interpreted as plugin data.  The return
 value must be a table.  The table is stored in a `vifm.plugins.all` dictionary
 with a key that corresponds to the plugin's name.  At the moment the name of a
-plugin is the name of its directory.
+plugin is the name of its directory.  Name of a plugin must be unique (names
+that differ by only by case are considered unique) or loading it will be
+skipped.
 
 Global variables created by one plugin are local to that plugin and won't
 affect or be visible to other plugins.
@@ -202,6 +227,9 @@ relevant information on each invocation.  Things like keys or commands can't
 be handled this way because they need to communicate metadata about how to
 call them.
 
+Handlers are executed in a safe environment and can't call API marked as
+{unsafe}.
+
 A handler can be used in place of an external program in |vifm-:filetype| or
 |vifm-:filextype| commands like this: >
   filetype {*.png} #pluginname#open
@@ -213,8 +241,10 @@ command like this: >
 A viewer can also be called through |vifm-%q|: >
   :!#pluginname#something %q
 
-A handler can be used to generate |vifm-:'statusline'| value like this: >
+A handler can be used to generate |vifm-'statusline'| or |vifm-'tabline'|
+value like this: >
   set statusline=#pluginname#statusline
+  set tabline=#pluginname#tabline
 
 Below is a specification of input and output for different kinds of handlers.
 
@@ -260,6 +290,18 @@ Fields of returned table:
  - "format" (string)
    Value for |vifm-'statusline'|.
 
+'tabline' handler~
+
+Fields of {info} argument:
+ - "other" (boolean)
+   Whether this is for the tabline of inactive pane in case of pane tabs.
+ - "width" (integer)
+   Width of the tab line.
+
+Fields of returned table:
+ - "format" (string)
+   Value for |vifm-'tabline'|.
+
 'vicmd' or 'vixcmd' handler~
 
 Editor is invoked by Vifm in several different contexts with generally
@@ -350,6 +392,57 @@ Fields of {info} argument:
    Because such lists come from external tools, this information is determined
    using a simple heuristic and might not always be correct.
 
+--------------------------------------------------------------------------------
+*vifm-lua-events*
+
+"app.exit" event~
+
+Occurs before Vifm exits normally.  Does not happen on |vifm-:restart|.
+
+"app.fsop" event~
+
+Occurs after a successful execution of a file-system operation.
+
+Fields of the only table parameter:
+ - "op" (string)
+   Type of an operation (see below for a list).
+ - "path" (string)
+   Source path or the only path affected by the operation.
+ - "target" (string) (optional)
+   Destination path or target of a symbolic link.
+ - "isdir" (boolean)
+   Whether operation is performed on a directory.
+ - "fromtrash" (boolean) (optional)
+   "move" operation sets the flag if "path" points inside a trash directory,
+   meaning that file was restored or otherwise moved from trash.
+ - "totrash" (boolean) (optional)
+   "move" operation sets the flag if "target" points inside a trash directory,
+   meaning that file was deleted to trash.
+
+Operation types:
+ - "copy"
+   Copying a file/directory from one location to another.
+ - "create"
+   Creation of a file/directory.
+ - "move"
+   Moving a file/directory from one location to another.  Includes renames,
+   moves across partition boundaries, deletion to trash and moving out of
+   trash.
+ - "remove"
+   Removal of a file/directory.  This includes symbolic link removal on
+   changing its target.
+ - "symlink"
+   Creation of a symbolic link, which might be a second step of updating its
+   target.
+
+The list of operations isn't complete, to be extended as needed.
+
+Operations performed in background aren't reported.
+
+Note: a bulk rename that contains a cycle ("a" -> "b" -> "a" being the
+shortest one) will be reported the way it's performed, that is including
+temporary renames (e.g., "a -> a_", "b -> a", "a_ -> b").
+
 --------------------------------------------------------------------------------
 *vifm-lua-caps*
 
@@ -358,11 +451,14 @@ This is what one can do by using Lua at the moment:
  * save and reset options (e.g., some kind of presets) (|vifm-l_vifm.opts|)
  * custom columns (|vifm-l_vifm.addcolumntype()|)
  * :filetype/:filextype/:fileviewer/%q handlers in Lua (|vifm-lua-handlers|)
- * 'statusline' generator in Lua (|vifm-lua-handlers|)
+ * 'statusline' and 'tabline' generators in Lua (|vifm-lua-handlers|)
  * fully integrate an arbitrary editor by changing |vifm-'vicmd'| or |vifm-'vixcmd'|
    (|vifm-lua-handlers|), while builtin logic assumes compatibility with Vim
  * starting background jobs that appear on a job bar (|vifm-l_vifm.startjob()|)
+ * starting background jobs to do something when they're over (see "onexit" of
+   |vifm-l_vifm.startjob()|)
  * defining custom keys and selectors (|vifm-l_vifm.keys.add()|)
+ * running code before exit and when a file operation is done (|vifm-lua-events|)
 
 --------------------------------------------------------------------------------
 *vifm-lua-api*
@@ -473,13 +569,13 @@ Raises an error:~
   If {handler}.name has incorrect value.
 
 vifm.currview()                                *vifm-l_vifm.currview()*
-Retrieves a reference to current view.
+Retrieves a reference to active view.
 
 Return:~
   Returns an instance of |vifm-l_VifmView|.
 
 vifm.otherview()                               *vifm-l_vifm.otherview()*
-Retrieves a reference to current view.
+Retrieves a reference to inactive view.
 
 Return:~
   Returns an instance of |vifm-l_VifmView|.
@@ -522,6 +618,18 @@ Parameters:~
 Return:~
   Modified path.
 
+vifm.executable({what})                        *vifm-l_vifm.executable()*
+If {what} is absolute or relative path, checks whether it exists and refers to
+an executable, otherwise checks whether command named {what} is present in
+directories listed in $PATH.  Checks for various executable extensions on
+Windows.
+
+Parameters:~
+  {what}  Path or command name to check.
+
+Return:~
+  `true` if an executable was found.
+
 vifm.exists({path})                            *vifm-l_vifm.exists()*
 Checks existence of a path without resolving symbolic links.
 
@@ -586,8 +694,10 @@ Possible fields of {job}:
     - "r" - reading output of the process (see |vifm-l_VifmJob:stdout()|)
     - "w" - providing input to the process (see |vifm-l_VifmJob:stdin()|)
     - ""  - no I/O interaction
+ - "onexit" (function) (default: `nil`)
+   Handler to invoke when the job is done.  The handler is {delayed}.
  - "mergestreams" (boolean) (default: false)
-   Whether to merge error streams of the command with output stream.
+   Whether to merge error stream of the command with its output stream.
  - "visible" (boolean) (default: false)
    Whether to show this job on a job bar.
 
@@ -600,6 +710,27 @@ Return:~
 Raises an error:~
   If "iomode" has incorrect value.
 
+vifm.input({info})                             *vifm-l_vifm.input()*
+Prompts user for input via command-line prompt.
+
+Possible fields of {info}:
+ - "prompt" (string)
+   Prompt invitation.
+ - "initial" (string) (default: "")
+   Initial input for the prompt.
+ - "complete" (string) (default: "")
+   How to complete input: "dir" (directories only), "file" (files and
+   directories), "" (no completion).
+
+Parameters:~
+  {info}  Table with information about the prompt.
+
+Return:~
+  String on success or nil if the prompt was cancelled.
+
+Raises an error:~
+  If {info}.complete has incorrect value.
+
 --------------------------------------------------------------------------------
 *vifm-l_vifm.cmds*
 
@@ -690,6 +821,31 @@ Parameters:~
 Return:~
   `true` on success.
 
+--------------------------------------------------------------------------------
+*vifm-l_vifm.events*
+
+This global `vifm.events` table groups items related to events.
+
+events.listen({event})                         *vifm-l_vifm.events.listen()*
+Registers a handler for an event.
+
+Possible fields of {event}:
+ - "event" (string)
+   Name of event to listen for.  See |vifm-lua-events| for event descriptions.
+ - "handler" (function)
+   Handler to invoke when the event happens.
+
+Registering the same handler more than once for the same event has no effect.
+
+Parameters:~
+  {event}  Table with information about event handling.
+
+Return:~
+  `nil`
+
+Raises an error:~
+  On unknown event name.
+
 --------------------------------------------------------------------------------
 *vifm-l_vifm.keys*
 
@@ -706,9 +862,9 @@ Possible fields of {key}:
  - "description" (string) (default: "")
    Description of the key.
  - "modes" (array of strings)
-   List of modes to register the key in.  Supported values: cmdline, normal,
-   visual, menus, view and dialogs (sort, attributes, change and file info).
-   Unsupported values are ignored.
+   List of modes to register the key in.  Supported values: cmdline, nav,
+   normal, visual, menus, view and dialogs (sort, attributes, change and file
+   info).  Unsupported values are ignored.
  - "isselector" (boolean) (default: false)
    Whether this handler defines a selector rather than a regular key.
  - "followedby" (string) (default: "none")
@@ -731,13 +887,13 @@ Fields of {info} argument for {key}.handler:
    Key argument (e.g., "x" in "mx" sequence).
 
 Fields of table returned by {key}.handler for selectors:
- - "indexes" (table)
+ - "indexes" (array of integers)
    Table of indexes of entries of the current view (|vifm-l_vifm.currview()|)
-   that were selected for the operation.  Out of range values and duplicates
-   are silently ignored.  Indexes are sorted before processing.
+   that were selected for the operation.  Invalid, nonexistent and duplicate
+   indexes are silently ignored.  Indexes are sorted before processing.
 
 Parameters:~
-  {cmd}  Table with information about a key.
+  {key}  Table with information about a key.
 
 Return:~
   `true` on success.
@@ -770,6 +926,7 @@ Full path to plugin's root directory.
 
 plugin.require({modname})                      *vifm-l_vifm.plugin.require()*
 Plugin-specific `require`, which loads Lua modules relative to plugin's root.
+Does not create a global variable for the module.
 
 This function is {unsafe}.
 
@@ -881,14 +1038,14 @@ version.app.str (string)                       *vifm-l_vifm.version.app.str*
 Version of Vifm as a string.
 
 version.api.major (integer)                    *vifm-l_vifm.version.api.major*
-Major version of Lua API.  0 now, should become 1 when API design settles and
+Major version of Lua API.  Should become 1 when API design settles and then
 remain at 1.
 
 version.api.minor (integer)                    *vifm-l_vifm.version.api.minor*
-Minor version of Lua API.  0 now.
+Minor version of Lua API.
 
 version.api.patch (integer)                    *vifm-l_vifm.version.api.patch*
-Patch version of Lua API.  0 now.
+Patch version of Lua API.
 
 version.api.has({feature})                     *vifm-l_vifm.version.api.has()*
 Checks presence of a feature.  There are no features to test yet.
@@ -951,6 +1108,9 @@ the Unix epoch.
 VifmEntry.type (string)                        *vifm-l_VifmEntry.type*
 Type of the entry.  See |vifm-filetype()| for the list of values.
 
+VifmEntry.isdir (boolean)                      *vifm-l_VifmEntry.isdir*
+True if an entry is either a directory or a symbolic link to a directory.
+
 VifmEntry.folded (boolean)                     *vifm-l_VifmEntry.folded*
 Whether this entry is folded.
 
@@ -997,7 +1157,7 @@ Raises an error:~
   If waiting has failed.
 
 VifmJob:exitcode()                             *vifm-l_VifmJob:exitcode()*
-Retrieves exit code of the application.
+Retrieves exit code of the command.
 
 Waits for the job to finish.
 
@@ -1010,7 +1170,7 @@ Raises an error:~
   If waiting has failed.
 
 VifmJob:stdin()                                *vifm-l_VifmJob:stdin()*
-Retrieves stream associated with standard input of a job.
+Retrieves stream associated with standard input of the job.
 
 Return:~
   Returns file stream from standard I/O library of Lua.
@@ -1020,7 +1180,7 @@ Raises an error:~
   If input stream object is already closed.
 
 VifmJob:stdout()                               *vifm-l_VifmJob:stdout()*
-Retrieves stream associated with standard output of a job.  Includes error
+Retrieves stream associated with standard output of the job.  Includes error
 stream if `mergestreams` was set to `true`.
 
 Return:~
@@ -1067,7 +1227,7 @@ Return:~
 VifmTab:getview([{query}])                     *vifm-l_VifmTab:getview()*
 
 Possible fields of {query}:
- - "pane" (integer) (default: 1)
+ - "pane" (integer) (default: index of active pane)
    Which view to get from a global tab.  1 is left/top pane, 2 is right/bottom
    one.  Ignored for a pane tab.
 
@@ -1107,6 +1267,16 @@ Location of the current view.
 VifmView.entrycount (integer)                  *vifm-l_VifmView.entrycount*
 Number of entries in the view.
 
+VifmView.custom (table)                        *vifm-l_VifmView.custom*
+Table with information about custom file list.  The value is `nil` if custom
+view isn't active.
+
+Fields:
+ - "title" (string)
+   Title of a custom view which usually describes how it was created.
+ - "type" (string)
+   Values returned by |vifm-getpanetype()| except for "regular".
+
 VifmView:cd({path})                            *vifm-l_VifmView:cd()*
 Changes location of the view.  {path} isn't expanded in any way.
 
@@ -1127,5 +1297,33 @@ Parameters:~
 Return:~
   |vifm-l_VifmEntry| on success and `nil` on wrong index.
 
+VifmView:select({entries})                     *vifm-l_VifmView:select()*
+Selects entries.  Does nothing in visual non-amend mode.  See
+|vifm-l_VifmView:unselect()| to unselect entries.
+
+This function is {unsafe}.
+
+Possible fields of {entries}:
+ - "indexes" (array of integers)
+   Indexes of entries to select.  Invalid and nonexistent indexes are
+   silently ignored.
+
+Return:~
+  Number of new selected entries.
+
+VifmView:unselect({entries})                   *vifm-l_VifmView:unselect()*
+Unselects entries.  Does nothing in visual non-amend mode.  See
+|vifm-l_VifmView:select()| to select entries.
+
+This function is {unsafe}.
+
+Possible fields of {entries}:
+ - "indexes" (array of integers)
+   Indexes of entries to unselect.  Invalid and nonexistent indexes are
+   silently ignored.
+
+Return:~
+  Number of new unselected entries.
+
 --------------------------------------------------------------------------------
  vim:tw=78:fo=tcq2:isk=!-~,^*,^\|,^\":ts=8:ft=help:norl:
diff --git a/data/vim/doc/plugin/vifm-plugin.txt b/data/vim/doc/plugin/vifm-plugin.txt
index 54b0edf..57057e6 100644
--- a/data/vim/doc/plugin/vifm-plugin.txt
+++ b/data/vim/doc/plugin/vifm-plugin.txt
@@ -1,4 +1,4 @@
-*vifm-plugin.txt*    For Vifm version 0.12.1  Last change: 2022 Sep 21
+*vifm-plugin.txt*    For Vifm version 0.13  Last change: 2023 Apr 04
 
  Email for bugs and suggestions: <xaizek@posteo.net>
 
diff --git a/data/vim/syntax/vifm.vim b/data/vim/syntax/vifm.vim
index f001705..7f1a5bc 100644
--- a/data/vim/syntax/vifm.vim
+++ b/data/vim/syntax/vifm.vim
@@ -1,6 +1,6 @@
 " vifm syntax file
 " Maintainer:  xaizek <xaizek@posteo.net>
-" Last Change: September 18, 2022
+" Last Change: March 10, 2023
 " Inspired By: Vim syntax file by Dr. Charles E. Campbell, Jr.
 
 if exists('b:current_syntax')
@@ -19,12 +19,12 @@ syntax keyword vifmCommand contained
 		\ dirs e[dit] el[se] empty en[dif] exi[t] file fin[d] fini[sh] go[to] gr[ep]
 		\ h[elp] hideui histnext his[tory] histprev keepsel jobs locate ls lstrash
 		\ marks media mes[sages] mkdir m[ove] noh[lsearch] on[ly] plugin plugins
-		\ popd pushd pu[t] pw[d] qa[ll] q[uit] redr[aw] reg[isters] regular rename
-		\ restart restore rlink screen sh[ell] siblnext siblprev sor[t] sp[lit]
-		\ st[op] s[ubstitute] tabc[lose] tabm[ove] tabname tabnew tabn[ext]
-		\ tabo[nly] tabp[revious] touch tr trashes tree session sync undol[ist]
-		\ ve[rsion] vie[w] vifm vs[plit] winc[md] w[rite] wq wqa[ll] xa[ll] x[it]
-		\ y[ank]
+		\ popd pushd pu[t] pw[d] qa[ll] q[uit] redr[aw] rege[dit] reg[isters]
+		\ regular rename restart restore rlink screen sh[ell] siblnext siblprev
+		\ sor[t] sp[lit] st[op] s[ubstitute] tabc[lose] tabm[ove] tabname tabnew
+		\ tabn[ext] tabo[nly] tabp[revious] touch tr trashes tree session sync
+		\ undol[ist] ve[rsion] vie[w] vifm vs[plit] winc[md] w[rite] wq wqa[ll]
+		\ xa[ll] x[it] y[ank]
 		\ nextgroup=vifmArgs
 syntax keyword vifmCommandCN contained
 		\ alink apropos bmark bmarks bmgo cds change chmod chown clone compare
@@ -32,20 +32,20 @@ syntax keyword vifmCommandCN contained
 		\ dirs e[dit] el[se] empty en[dif] exi[t] file fin[d] fini[sh] go[to] gr[ep]
 		\ h[elp] hideui histnext his[tory] histprev jobs locate ls lstrash marks
 		\ media mes[sages] mkdir m[ove] noh[lsearch] on[ly] popd pushd pu[t] pw[d]
-		\ qa[ll] q[uit] redr[aw] reg[isters] regular rename restart restore rlink
-		\ screen sh[ell] siblnext siblprev sor[t] sp[lit] s[ubstitute] tabc[lose]
-		\ tabm[ove] tabname tabnew tabn[ext] tabo[nly] tabp[revious] touch tr
-		\ trashes tree session sync undol[ist] ve[rsion] vie[w] vifm vs[plit]
-		\ winc[md] w[rite] wq wqa[ll] xa[ll] x[it] y[ank]
+		\ qa[ll] q[uit] redr[aw] rege[dit] reg[isters] regular rename restart restore
+		\ rlink screen sh[ell] siblnext siblprev sor[t] sp[lit] s[ubstitute]
+		\ tabc[lose] tabm[ove] tabname tabnew tabn[ext] tabo[nly] tabp[revious]
+		\ touch tr trashes tree session sync undol[ist] ve[rsion] vie[w] vifm
+		\ vs[plit] winc[md] w[rite] wq wqa[ll] xa[ll] x[it] y[ank]
 		\ nextgroup=vifmArgsCN
 
 " commands that might be prepended to a command without changing everything else
 syntax keyword vifmPrefixCommands contained windo winrun
 
 " Map commands
-syntax keyword vifmMap contained dm[ap] dn[oremap] du[nmap] map mm[ap]
-		\ mn[oremap] mu[nmap] nm[ap] nn[oremap] no[remap] nun[map] qm[ap] qn[oremap]
-		\ qun[map] unm[ap] vm[ap] vn[oremap] vu[nmap]
+syntax keyword vifmMap contained amap anoremap aunmap dm[ap] dn[oremap] du[nmap]
+		\ map mm[ap] mn[oremap] mu[nmap] nm[ap] nn[oremap] no[remap] nun[map] qm[ap]
+		\ qn[oremap] qun[map] unm[ap] vm[ap] vn[oremap] vu[nmap]
 		\ skipwhite nextgroup=vifmMapArgs
 syntax keyword vifmCMapAbbr contained ca[bbrev] cm[ap] cnorea[bbrev] cno[remap]
 		\ cuna[bbrev] cu[nmap]
@@ -77,7 +77,7 @@ syntax case match
 
 " Builtin functions
 syntax match vifmBuiltinFunction
-		\ '\(chooseopt\|expand\|executable\|extcached\|filetype\|fnameescape\|getpanetype\|has\|layoutis\|paneisat\|system\|tabpagenr\|term\)\ze('
+		\ '\(chooseopt\|expand\|executable\|extcached\|filereadable\|filetype\|fnameescape\|getpanetype\|has\|input\|layoutis\|paneisat\|system\|tabpagenr\|term\)\ze('
 
 " Operators
 syntax match vifmOperator "\(==\|!=\|>=\?\|<=\?\|\.\|-\|+\|&&\|||\)" skipwhite
@@ -90,7 +90,8 @@ syntax keyword vifmHiGroups contained WildMenu Border Win CmdLine CurrLine
 		\ TopLine TopLineSel StatusLine JobLine SuggestBox Fifo ErrorMsg
 		\ CmpMismatch CmpUnmatched CmpBlank
 		\ AuxWin OtherWin TabLine TabLineSel HardLink LineNr OddLine
-		\ User1 User2 User3 User4 User5 User6 User7 User8 User9
+		\ User1 User2 User3 User4 User5 User6 User7 User8 User9 User10
+		\ User11 User12 User13 User14 User15 User16 User17 User18 User19 User20
 syntax keyword vifmHiStyles contained
 		\ bold underline reverse inverse standout italic combine none
 syntax keyword vifmHiColors contained black red green yellow blue magenta cyan
@@ -142,15 +143,15 @@ syntax keyword vifmOption contained aproposprg autocd autochpos caseoptions
 		\ cvoptions deleteprg dotdirs dotfiles dirsize fastrun fillchars fcs findprg
 		\ followlinks fusehome gdefault grepprg histcursor history hi hlsearch hls
 		\ iec ignorecase ic iooptions incsearch is laststatus lines locateprg ls
-		\ lsoptions lsview mediaprg milleroptions millerview mintimeoutlen number nu
-		\ numberwidth nuw previewoptions previewprg quickview relativenumber rnu
-		\ rulerformat ruf runexec scrollbind scb scrolloff sessionoptions ssop so
-		\ sort sortgroups sortorder sortnumbers shell sh shellflagcmd shcf shortmess
-		\ shm showtabline stal sizefmt slowfs smartcase scs statusline stl
-		\ suggestoptions syncregs syscalls tablabel tabprefix tabscope tabstop
-		\ tabsuffix timefmt timeoutlen title tm trash trashdir ts tuioptions to
-		\ undolevels ul vicmd viewcolumns vifminfo vimhelp vixcmd wildmenu wmnu
-		\ wildstyle wordchars wrap wrapscan ws
+		\ lsoptions lsview mediaprg milleroptions millerview mintimeoutlen mouse
+		\ navoptions number nu numberwidth nuw previewoptions previewprg quickview
+		\ relativenumber rnu rulerformat ruf runexec scrollbind scb scrolloff
+		\ sessionoptions ssop so sort sortgroups sortorder sortnumbers shell sh
+		\ shellflagcmd shcf shortmess shm showtabline stal sizefmt slowfs smartcase
+		\ scs statusline stl suggestoptions syncregs syscalls tablabel tabline
+		\ tabprefix tabscope tabstop tabsuffix tal timefmt timeoutlen title tm trash
+		\ trashdir ts tuioptions to undolevels ul vicmd viewcolumns vifminfo vimhelp
+		\ vixcmd wildmenu wmnu wildstyle wordchars wrap wrapscan ws
 
 " Disabled boolean options
 syntax keyword vifmOption contained noautocd noautochpos nocf nochaselinks
@@ -212,10 +213,13 @@ syntax region vifmInvertCommandSt start='\(\s\|:\)*invert\>' end='$\||'
 syntax region vifmInvertCommandStN start='\(\s\|:\)*invert\>' end='$\||'
 		\ contained keepend oneline contains=vifmInvertCommand,vifmNotation
 syntax region vifmSoCommandSt start='\(\s\|:\)*so\%[urce]\>' end='$\||'
-		\ keepend oneline contains=vifmSoCommand,vifmEnvVar,vifmStringInExpr
+		\ keepend oneline
+		\ contains=vifmSoCommand,vifmEnvVar,vifmStringInExpr,vifmComment
+		\,vifmInlineComment
 syntax region vifmSoCommandStN start='\(\s\|:\)*so\%[urce]\>' end='$\||'
 		\ contained keepend oneline
 		\ contains=vifmSoCommand,vifmEnvVar,vifmNotation,vifmStringInExpr
+		\,vifmComment,vifmInlineComment
 syntax region vifmMarkCommandSt start='^\(\s\|:\)*ma\%[rk]\>' end='$' keepend
 		\ oneline contains=vifmMarkCommand
 syntax region vifmCdCommandSt start='\(\s\|:\)*cd\>' end='$\||' keepend oneline
diff --git a/debian/changelog b/debian/changelog
index 2bd1d37..a02f4c1 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,5 +1,6 @@
-vifm (0.12.1-1) UNRELEASED; urgency=medium
+vifm (0.13-1) UNRELEASED; urgency=medium
 
+  [ Ondřej Nový ]
   * New upstream release.
   * d/watch: Fix GitHub.
   * Bump standards version to 4.6.2.
@@ -9,7 +10,10 @@ vifm (0.12.1-1) UNRELEASED; urgency=medium
   * Build-Depends: libncursesw5-dev -> libncurses-dev.
   * Don't remove debian/vifm/usr/share/vifm/vim-doc (Closes: #1024875).
 
- -- Ondřej Nový <onovy@debian.org>  Tue, 28 Feb 2023 09:10:56 +0100
+  [ Debian Janitor ]
+  * New upstream release.
+
+ -- Ondřej Nový <onovy@debian.org>  Tue, 22 Aug 2023 19:19:00 -0000
 
 vifm (0.12-1) unstable; urgency=medium
 
diff --git a/debian/patches/perl-interpreter-fix.patch b/debian/patches/perl-interpreter-fix.patch
index acafb09..edbaf8d 100644
--- a/debian/patches/perl-interpreter-fix.patch
+++ b/debian/patches/perl-interpreter-fix.patch
@@ -1,16 +1,20 @@
 Description: Use /usr/bin/perl as Perl interpreter
 Author: Ondřej Nový <onovy@debian.org>
 
---- a/src/vifm-convert-dircolors
-+++ b/src/vifm-convert-dircolors
+Index: vifm.git/src/vifm-convert-dircolors
+===================================================================
+--- vifm.git.orig/src/vifm-convert-dircolors
++++ vifm.git/src/vifm-convert-dircolors
 @@ -1,4 +1,4 @@
 -#!/usr/bin/env perl
 +#!/usr/bin/perl
  
  # vifm
  # Copyright (C) 2015 xaizek.
---- a/src/helpztags
-+++ b/src/helpztags
+Index: vifm.git/src/helpztags
+===================================================================
+--- vifm.git.orig/src/helpztags
++++ vifm.git/src/helpztags
 @@ -1,4 +1,4 @@
 -#!/usr/bin/env perl
 +#!/usr/bin/perl
diff --git a/pkgs/AppImage/genappimage.sh b/pkgs/AppImage/genappimage.sh
index d730704..f2d8862 100755
--- a/pkgs/AppImage/genappimage.sh
+++ b/pkgs/AppImage/genappimage.sh
@@ -37,13 +37,13 @@ cd "$BUILD_DIR"
 mkdir -p "$BUILD_DIR/AppDir/usr"
 
 # Obtain and compile libncursesw6 so that we get 256 color support
-wget http://ftp.gnu.org/gnu/ncurses/ncurses-6.2.tar.gz
-tar -xf ncurses-6.2.tar.gz
-NCURSES_DIR="$PWD/ncurses-6.2"
+wget http://ftp.gnu.org/gnu/ncurses/ncurses-6.4.tar.gz
+tar -xf ncurses-6.4.tar.gz
+NCURSES_DIR="$PWD/ncurses-6.4"
 pushd "$NCURSES_DIR"
 ./configure --without-shared --enable-widec --prefix=/ \
     --without-normal --without-debug --without-cxx --without-cxx-binding \
-    --without-ada --without-manpages --without-tests --without-gpm
+    --without-ada --without-manpages --without-tests
 make -j4
 make DESTDIR="$PWD/build" install
 popd
diff --git a/scripts/deploy b/scripts/deploy
index 9d56a81..8d6b3f9 100755
--- a/scripts/deploy
+++ b/scripts/deploy
@@ -49,8 +49,9 @@ fi
 if [ "$target" = all ] || [ "$target" = update ]; then
     echo "Updating version number..."
 
-    # update version in ChangeLog
-    sed -i "1s/current/$ver ($(date '+%Y-%m-%d'))/" ChangeLog
+    # update version in ChangeLogs
+    sed -i "s/to current\$/to $ver ($(date '+%Y-%m-%d'))/" \
+           ChangeLog ChangeLog.LuaAPI
 
     # update version in configure script
     sed -i "/AC_INIT(/s/, [^,]\\+/, $ver/" configure.ac
@@ -152,7 +153,7 @@ if [ "$target" = all ] || [ "$target" = archive ]; then
         rmdir "$dir/data/vim/doc/plugin"
         mkdir "$dir/data/vim-doc"
         mv "$dir/data/vim/doc/app" "$dir/data/vim-doc/doc"
-        pkgfiles='AUTHORS BUGS ChangeLog COPYING COPYING.3party FAQ INSTALL NEWS README THANKS TODO'
+        pkgfiles='AUTHORS BUGS ChangeLog ChangeLog.LuaAPI COPYING COPYING.3party FAQ INSTALL NEWS README THANKS TODO'
         for i in $pkgfiles; do
             dest="$dir/$i.txt"
             cp "$i" "$dest"
diff --git a/scripts/uncov-coverage b/scripts/uncov-coverage
index a610411..2c6029d 100755
--- a/scripts/uncov-coverage
+++ b/scripts/uncov-coverage
@@ -37,7 +37,11 @@ git clean --force -d ..
 git checkout --force stash@{0}^
 git stash pop
 
-find -name '*.gcda' -delete
+# need to remove *.gcda in tests as well, otherwise libgcov in tests that fork
+# can print "overwriting an existing profile data with a different timestamp" to
+# error stream and break tests
+find . ../tests -name '*.gcda' -delete
+
 make -C ../tests build
 make check
 
diff --git a/src/Makefile.am b/src/Makefile.am
index 123b688..219036f 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -30,7 +30,7 @@ dist_sample_colors__DATA = ../data/colors/astrell-root.vifm \
 									 ../data/colors/reicheltd-light.vifm \
 									 ../data/colors/Default-256.vifm
 dist_vim_doc__DATA = ../data/vim/doc/plugin/vifm-plugin.txt
-nodist_vim_doc__DATA = $(abs_srcdir)/../data/vim/doc/plugin/tags
+nodist_vim_doc__DATA = $(top_srcdir)/data/vim/doc/plugin/tags
 dist_vim_ftdetect__DATA = ../data/vim/ftdetect/vifm.vim \
 										 ../data/vim/ftdetect/vifm-rename.vim
 dist_vim_ftplugin__DATA = ../data/vim/ftplugin/vifm.vim \
@@ -44,7 +44,7 @@ dist_global_colors__DATA = ../data/colors/Default-256.vifm
 
 dist_vimdoc_doc__DATA = ../data/vim/doc/app/vifm-app.txt \
 												../data/vim/doc/app/vifm-lua.txt
-nodist_vimdoc_doc__DATA = $(abs_srcdir)/../data/vim/doc/app/tags
+nodist_vimdoc_doc__DATA = $(top_srcdir)/data/vim/doc/app/tags
 
 dist_pkgdata_DATA = ../data/vifmrc@DATA_SUFFIX@
 dist_pkgdata_SCRIPTS = ../data/vifm-media@DATA_SUFFIX@
@@ -61,10 +61,10 @@ dist_man_MANS = ../data/man/vifm.1 \
 
 MOSTLYCLEANFILES = compile_info.c \
 									 ../data/vifm-help.txt \
-									 $(abs_srcdir)/../data/vim/doc/app/tags \
-									 $(abs_srcdir)/../data/vim/doc/plugin/tags
+									 $(top_srcdir)/data/vim/doc/app/tags \
+									 $(top_srcdir)/data/vim/doc/plugin/tags
 clean-local:
-	if [ '$(abs_builddir)' != '$(abs_srcdir)' ]; then \
+	if [ '$(builddir)' != '$(srcdir)' ]; then \
 		$(RM) tags.c; \
 	fi
 
@@ -76,6 +76,14 @@ desktoppixdir = $(datadir)/pixmaps
 desktoppixfile = ../data/graphics/vifm.png
 dist_desktoppix_DATA = $(desktoppixfile)
 
+hicolor128dir = $(datadir)/icons/hicolor/128x128/apps
+hicolor128file = ../data/graphics/vifm.png
+dist_hicolor128_DATA = $(hicolor128file)
+
+hicolorsvgdir = $(datadir)/icons/hicolor/scalable/apps
+hicolorsvgfile = ../data/graphics/vifm.svg
+dist_hicolorsvg_DATA = $(hicolorsvgfile)
+
 bashcompldir = $(datadir)/bash-completion/completions
 bashcomplfile = ../data/shell-completion/bash/vifm
 dist_bashcompl_DATA = $(bashcomplfile)
@@ -88,8 +96,8 @@ fishcompldir = $(datadir)/fish/vendor_completions.d
 fishcomplfile = ../data/shell-completion/fish/vifm.fish
 dist_fishcompl_DATA = $(fishcomplfile)
 
-docsfile = ../AUTHORS ../BUGS ../COPYING ../ChangeLog ../FAQ ../INSTALL \
-					 ../NEWS ../README ../TODO
+docsfile = ../AUTHORS ../BUGS ../COPYING ../ChangeLog ../ChangeLog.LuaAPI \
+					 ../FAQ ../INSTALL ../NEWS ../README ../TODO
 docsdir = $(docdir)
 dist_docs_DATA = $(docsfile)
 
@@ -184,7 +192,9 @@ vifm_SOURCES = \
 	\
 	lua/api.h \
 	lua/common.c lua/common.h \
+	lua/vifm.c lua/vifm.h \
 	lua/vifm_cmds.c lua/vifm_cmds.h \
+	lua/vifm_events.c lua/vifm_events.h \
 	lua/vifm_handlers.c lua/vifm_handlers.h \
 	lua/vifm_keys.c lua/vifm_keys.h \
 	lua/vifm_tabs.c lua/vifm_tabs.h \
@@ -194,6 +204,7 @@ vifm_SOURCES = \
 	lua/vifmtab.c lua/vifmtab.h \
 	lua/vifmview.c lua/vifmview.h \
 	lua/vlua.c lua/vlua.h \
+	lua/vlua_cbacks.c lua/vlua_cbacks.h \
 	lua/vlua_state.c lua/vlua_state.h \
 	\
 	menus/all.h \
@@ -374,72 +385,74 @@ VIM = @VIM_PROG@
 UNDER_VCS = @IN_GIT_REPO@
 
 runtests:
-	echo 'mkdir -p $(abs_builddir)/../tests/' > $@_
+	$(AM_V_GEN)echo 'mkdir -p "$(abs_top_builddir)/tests/"' > $@_ && \
 	echo \
-		'$(MAKE) -C $(abs_srcdir)/../tests B=$(abs_builddir)/../tests/ CC="$(CC)"' \
-		>> $@_
-	chmod +x $@_
+		'$(MAKE) -C "$(abs_top_srcdir)/tests" B="$(abs_top_builddir)/tests/" CC="$(CC)" "$$@"' \
+		>> $@_ && \
+	chmod +x $@_ && \
 	mv $@_ $@
 
 coverage: check
-	lcov --directory $(abs_builddir)/ --base-directory . \
-	     --capture --output-file $(abs_builddir)/lcov-all.info --config-file \
-	     $(abs_srcdir)/lcovrc --test-name unit_tests --quiet
-	lcov --remove $(abs_builddir)/lcov-all.info -o $(abs_builddir)/lcov.info \
+	lcov --directory $(builddir)/ --base-directory . \
+	     --capture --output-file $(builddir)/lcov-all.info --config-file \
+	     $(srcdir)/lcovrc --test-name unit_tests --quiet
+	lcov --remove $(builddir)/lcov-all.info -o $(builddir)/lcov.info \
 	     '/usr/*'
-	genhtml --output-directory $(abs_builddir)/cov-report/ \
-	     $(abs_builddir)/lcov.info --config-file $(abs_srcdir)/lcovrc \
-	     --show-details --css-file $(abs_srcdir)/lcov.css
+	genhtml --output-directory $(builddir)/cov-report/ \
+	     $(builddir)/lcov.info --config-file $(srcdir)/lcovrc \
+	     --show-details --css-file $(srcdir)/lcov.css
 
 CLEANFILES = runtests lcov-all.info lcov.info $(vifm_OBJECTS:.o=.gcno) \
                                               $(vifm_OBJECTS:.o=.gcda)
 
 distclean-local:
-	$(MAKE) -C $(abs_srcdir)/../tests B=$(abs_builddir)/../tests/ clean
+	$(MAKE) -C "$(abs_top_srcdir)/tests" B="$(abs_top_builddir)/tests/" clean
 	$(RM) -r cov-report/
 
-version.o: $(filter-out version.o, $(vifm_OBJECTS))
+version.o: $(vifm_OBJECTS:version.o=)
 compile_info.c: update_compile_info
-	@$(abs_srcdir)/update-compile-info $(UNDER_VCS)
+	@$(srcdir)/update-compile-info $(UNDER_VCS)
 
 # No action needed for this target.
 update_compile_info:
 
-../data/vifm-help.txt: $(abs_srcdir)/../data/man/vifm.1
+../data/vifm-help.txt: $(top_srcdir)/data/man/vifm.1
 	$(AM_V_GEN)mkdir -p ../data/; \
 	if [ -n "$(MANGEN)" -a -n "$(SED)" ]; then \
 		if [ -n "$(COL)" ]; then \
-			$(MANGEN) -Tascii -man $(abs_srcdir)/../data/man/vifm.1 | $(SED) -e 's/\x1b\[[0-9]*m//g' -e 's/\x0d//g' | $(COL) -b >| '$@'; \
+			$(MANGEN) -Tascii -man $(top_srcdir)/data/man/vifm.1 | $(SED) -e 's/\x1b\[[0-9]*m//g' -e 's/\x0d//g' | $(COL) -b >| '$@'; \
 		else \
-			$(MANGEN) -Tascii -man $(abs_srcdir)/../data/man/vifm.1 | $(SED) -e 's/.\x08//g' -e 's/\x1b\[[0-9]*m//g' -e 's/\x0d//g' >| '$@'; \
+			$(MANGEN) -Tascii -man $(top_srcdir)/data/man/vifm.1 | $(SED) -e 's/.\x08//g' -e 's/\x1b\[[0-9]*m//g' -e 's/\x0d//g' >| '$@'; \
 		fi \
 	fi
 
-$(abs_srcdir)/../data/vim/doc/app/tags: \
-                                  $(abs_srcdir)/../data/vim/doc/app/vifm-app.txt \
-                                  $(abs_srcdir)/../data/vim/doc/app/vifm-lua.txt
+$(top_srcdir)/data/vim/doc/app/tags: \
+                                   $(top_srcdir)/data/vim/doc/app/vifm-app.txt \
+                                   $(top_srcdir)/data/vim/doc/app/vifm-lua.txt
 	$(AM_V_GEN)mkdir -p ../data/vim/doc/app/; \
 	if [ -n "$(PERL)" ]; then \
-		$(abs_srcdir)/helpztags "$(abs_srcdir)/../data/vim/doc/app"; \
+		$(srcdir)/helpztags "$(top_srcdir)/data/vim/doc/app"; \
 	elif [ -n "$(VIM)" ]; then \
-		vim -e -s -c 'helptags $(abs_srcdir)/../data/vim/doc/app|q'; \
+		vim -e -s -c 'helptags $(top_srcdir)/data/vim/doc/app|q'; \
 	else \
-		touch $@; \
+		echo "Can't generate tags without perl or vim"; \
+		false; \
 	fi
 
-$(abs_srcdir)/../data/vim/doc/plugin/tags: \
-                            $(abs_srcdir)/../data/vim/doc/plugin/vifm-plugin.txt
+$(top_srcdir)/data/vim/doc/plugin/tags: \
+                               $(top_srcdir)/data/vim/doc/plugin/vifm-plugin.txt
 	$(AM_V_GEN)mkdir -p ../data/vim/doc/plugin/; \
 	if [ -n "$(PERL)" ]; then \
-		$(abs_srcdir)/helpztags "$(abs_srcdir)/../data/vim/doc/plugin"; \
+		$(srcdir)/helpztags "$(top_srcdir)/data/vim/doc/plugin"; \
 	elif [ -n "$(VIM)" ]; then \
-		vim -e -s -c 'helptags $(abs_srcdir)/../data/vim/doc/plugin|q'; \
+		vim -e -s -c 'helptags $(top_srcdir)/data/vim/doc/plugin|q'; \
 	else \
-		touch $@; \
+		echo "Can't generate tags without perl or vim"; \
+		false; \
 	fi
 
-tags.c: $(abs_srcdir)/../data/vim/doc/app/tags
-	$(AM_V_GEN)if [ -s $(abs_srcdir)/../data/vim/doc/app/tags -a -n "$(AWK)" ]; then \
+tags.c: $(top_srcdir)/data/vim/doc/app/tags
+	$(AM_V_GEN)if [ -s $(top_srcdir)/data/vim/doc/app/tags -a -n "$(AWK)" ]; then \
 		$(AWK) ' \
 			BEGIN { \
 				print "const char *tags[] = {" \
@@ -450,8 +463,8 @@ tags.c: $(abs_srcdir)/../data/vim/doc/app/tags
 			END { \
 				print "\t0,\n};" \
 			} \
-		' $(abs_srcdir)/../data/vim/doc/app/tags > $@; \
-	elif [ '$(abs_builddir)' != '$(abs_srcdir)' ]; then \
+		' $(top_srcdir)/data/vim/doc/app/tags > $@; \
+	elif [ '$(builddir)' != '$(srcdir)' ]; then \
 		echo 'const char *tags[] = {};' > $@; \
 	fi
 
diff --git a/src/Makefile.in b/src/Makefile.in
index 22f2d24..e29b476 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -95,6 +95,7 @@ subdir = src
 ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
 am__aclocal_m4_deps =  \
 	$(top_srcdir)/build-aux/m4/ax_check_compile_flag.m4 \
+	$(top_srcdir)/build-aux/m4/ax_pthread.m4 \
 	$(top_srcdir)/configure.ac
 am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
 	$(ACLOCAL_M4)
@@ -102,7 +103,8 @@ DIST_COMMON = $(srcdir)/Makefile.am $(dist_bin_SCRIPTS) \
 	$(dist_pkgdata_SCRIPTS) $(dist_bashcompl_DATA) \
 	$(dist_desktopapp_DATA) $(dist_desktoppix_DATA) \
 	$(dist_docs_DATA) $(dist_fishcompl_DATA) \
-	$(dist_global_colors__DATA) $(dist_pkgdata_DATA) \
+	$(dist_global_colors__DATA) $(dist_hicolor128_DATA) \
+	$(dist_hicolorsvg_DATA) $(dist_pkgdata_DATA) \
 	$(dist_sample_colors__DATA) $(dist_vim_autoload_vifm__DATA) \
 	$(dist_vim_doc__DATA) $(dist_vim_ftdetect__DATA) \
 	$(dist_vim_ftplugin__DATA) $(dist_vim_plugin__DATA) \
@@ -117,6 +119,7 @@ am__installdirs = "$(DESTDIR)$(bindir)" "$(DESTDIR)$(bindir)" \
 	"$(DESTDIR)$(bashcompldir)" "$(DESTDIR)$(desktopappdir)" \
 	"$(DESTDIR)$(desktoppixdir)" "$(DESTDIR)$(docsdir)" \
 	"$(DESTDIR)$(fishcompldir)" "$(DESTDIR)$(global_colors_dir)" \
+	"$(DESTDIR)$(hicolor128dir)" "$(DESTDIR)$(hicolorsvgdir)" \
 	"$(DESTDIR)$(pkgdatadir)" "$(DESTDIR)$(sample_colors_dir)" \
 	"$(DESTDIR)$(vim_autoload_vifm_dir)" \
 	"$(DESTDIR)$(vim_doc_dir)" "$(DESTDIR)$(vim_ftdetect_dir)" \
@@ -160,24 +163,26 @@ am_vifm_OBJECTS = cfg/config.$(OBJEXT) cfg/info.$(OBJEXT) \
 	lua/lua/ltablib.$(OBJEXT) lua/lua/ltm.$(OBJEXT) \
 	lua/lua/lundump.$(OBJEXT) lua/lua/lutf8lib.$(OBJEXT) \
 	lua/lua/lvm.$(OBJEXT) lua/lua/lzio.$(OBJEXT) \
-	lua/common.$(OBJEXT) lua/vifm_cmds.$(OBJEXT) \
+	lua/common.$(OBJEXT) lua/vifm.$(OBJEXT) \
+	lua/vifm_cmds.$(OBJEXT) lua/vifm_events.$(OBJEXT) \
 	lua/vifm_handlers.$(OBJEXT) lua/vifm_keys.$(OBJEXT) \
 	lua/vifm_tabs.$(OBJEXT) lua/vifm_viewcolumns.$(OBJEXT) \
 	lua/vifmentry.$(OBJEXT) lua/vifmjob.$(OBJEXT) \
 	lua/vifmtab.$(OBJEXT) lua/vifmview.$(OBJEXT) \
-	lua/vlua.$(OBJEXT) lua/vlua_state.$(OBJEXT) \
-	menus/apropos_menu.$(OBJEXT) menus/bmarks_menu.$(OBJEXT) \
-	menus/cabbrevs_menu.$(OBJEXT) menus/colorscheme_menu.$(OBJEXT) \
-	menus/commands_menu.$(OBJEXT) menus/dirhistory_menu.$(OBJEXT) \
-	menus/dirstack_menu.$(OBJEXT) menus/filetypes_menu.$(OBJEXT) \
-	menus/find_menu.$(OBJEXT) menus/grep_menu.$(OBJEXT) \
-	menus/history_menu.$(OBJEXT) menus/jobs_menu.$(OBJEXT) \
-	menus/locate_menu.$(OBJEXT) menus/trash_menu.$(OBJEXT) \
-	menus/trashes_menu.$(OBJEXT) menus/map_menu.$(OBJEXT) \
-	menus/marks_menu.$(OBJEXT) menus/media_menu.$(OBJEXT) \
-	menus/menus.$(OBJEXT) menus/plugins_menu.$(OBJEXT) \
-	menus/registers_menu.$(OBJEXT) menus/undolist_menu.$(OBJEXT) \
-	menus/users_menu.$(OBJEXT) menus/vifm_menu.$(OBJEXT) \
+	lua/vlua.$(OBJEXT) lua/vlua_cbacks.$(OBJEXT) \
+	lua/vlua_state.$(OBJEXT) menus/apropos_menu.$(OBJEXT) \
+	menus/bmarks_menu.$(OBJEXT) menus/cabbrevs_menu.$(OBJEXT) \
+	menus/colorscheme_menu.$(OBJEXT) menus/commands_menu.$(OBJEXT) \
+	menus/dirhistory_menu.$(OBJEXT) menus/dirstack_menu.$(OBJEXT) \
+	menus/filetypes_menu.$(OBJEXT) menus/find_menu.$(OBJEXT) \
+	menus/grep_menu.$(OBJEXT) menus/history_menu.$(OBJEXT) \
+	menus/jobs_menu.$(OBJEXT) menus/locate_menu.$(OBJEXT) \
+	menus/trash_menu.$(OBJEXT) menus/trashes_menu.$(OBJEXT) \
+	menus/map_menu.$(OBJEXT) menus/marks_menu.$(OBJEXT) \
+	menus/media_menu.$(OBJEXT) menus/menus.$(OBJEXT) \
+	menus/plugins_menu.$(OBJEXT) menus/registers_menu.$(OBJEXT) \
+	menus/undolist_menu.$(OBJEXT) menus/users_menu.$(OBJEXT) \
+	menus/vifm_menu.$(OBJEXT) \
 	modes/dialogs/attr_dialog_nix.$(OBJEXT) \
 	modes/dialogs/change_dialog.$(OBJEXT) \
 	modes/dialogs/msg_dialog.$(OBJEXT) \
@@ -307,29 +312,30 @@ am__depfiles_remade = ./$(DEPDIR)/args.Po ./$(DEPDIR)/background.Po \
 	io/private/$(DEPDIR)/ioc.Po io/private/$(DEPDIR)/ioe.Po \
 	io/private/$(DEPDIR)/ioeta.Po io/private/$(DEPDIR)/ionotif.Po \
 	io/private/$(DEPDIR)/traverser.Po lua/$(DEPDIR)/common.Po \
-	lua/$(DEPDIR)/vifm_cmds.Po lua/$(DEPDIR)/vifm_handlers.Po \
+	lua/$(DEPDIR)/vifm.Po lua/$(DEPDIR)/vifm_cmds.Po \
+	lua/$(DEPDIR)/vifm_events.Po lua/$(DEPDIR)/vifm_handlers.Po \
 	lua/$(DEPDIR)/vifm_keys.Po lua/$(DEPDIR)/vifm_tabs.Po \
 	lua/$(DEPDIR)/vifm_viewcolumns.Po lua/$(DEPDIR)/vifmentry.Po \
 	lua/$(DEPDIR)/vifmjob.Po lua/$(DEPDIR)/vifmtab.Po \
 	lua/$(DEPDIR)/vifmview.Po lua/$(DEPDIR)/vlua.Po \
-	lua/$(DEPDIR)/vlua_state.Po lua/lua/$(DEPDIR)/lapi.Po \
-	lua/lua/$(DEPDIR)/lauxlib.Po lua/lua/$(DEPDIR)/lbaselib.Po \
-	lua/lua/$(DEPDIR)/lcode.Po lua/lua/$(DEPDIR)/lcorolib.Po \
-	lua/lua/$(DEPDIR)/lctype.Po lua/lua/$(DEPDIR)/ldblib.Po \
-	lua/lua/$(DEPDIR)/ldebug.Po lua/lua/$(DEPDIR)/ldo.Po \
-	lua/lua/$(DEPDIR)/ldump.Po lua/lua/$(DEPDIR)/lfunc.Po \
-	lua/lua/$(DEPDIR)/lgc.Po lua/lua/$(DEPDIR)/linit.Po \
-	lua/lua/$(DEPDIR)/liolib.Po lua/lua/$(DEPDIR)/llex.Po \
-	lua/lua/$(DEPDIR)/lmathlib.Po lua/lua/$(DEPDIR)/lmem.Po \
-	lua/lua/$(DEPDIR)/loadlib.Po lua/lua/$(DEPDIR)/lobject.Po \
-	lua/lua/$(DEPDIR)/lopcodes.Po lua/lua/$(DEPDIR)/loslib.Po \
-	lua/lua/$(DEPDIR)/lparser.Po lua/lua/$(DEPDIR)/lstate.Po \
-	lua/lua/$(DEPDIR)/lstring.Po lua/lua/$(DEPDIR)/lstrlib.Po \
-	lua/lua/$(DEPDIR)/ltable.Po lua/lua/$(DEPDIR)/ltablib.Po \
-	lua/lua/$(DEPDIR)/ltm.Po lua/lua/$(DEPDIR)/lundump.Po \
-	lua/lua/$(DEPDIR)/lutf8lib.Po lua/lua/$(DEPDIR)/lvm.Po \
-	lua/lua/$(DEPDIR)/lzio.Po menus/$(DEPDIR)/apropos_menu.Po \
-	menus/$(DEPDIR)/bmarks_menu.Po \
+	lua/$(DEPDIR)/vlua_cbacks.Po lua/$(DEPDIR)/vlua_state.Po \
+	lua/lua/$(DEPDIR)/lapi.Po lua/lua/$(DEPDIR)/lauxlib.Po \
+	lua/lua/$(DEPDIR)/lbaselib.Po lua/lua/$(DEPDIR)/lcode.Po \
+	lua/lua/$(DEPDIR)/lcorolib.Po lua/lua/$(DEPDIR)/lctype.Po \
+	lua/lua/$(DEPDIR)/ldblib.Po lua/lua/$(DEPDIR)/ldebug.Po \
+	lua/lua/$(DEPDIR)/ldo.Po lua/lua/$(DEPDIR)/ldump.Po \
+	lua/lua/$(DEPDIR)/lfunc.Po lua/lua/$(DEPDIR)/lgc.Po \
+	lua/lua/$(DEPDIR)/linit.Po lua/lua/$(DEPDIR)/liolib.Po \
+	lua/lua/$(DEPDIR)/llex.Po lua/lua/$(DEPDIR)/lmathlib.Po \
+	lua/lua/$(DEPDIR)/lmem.Po lua/lua/$(DEPDIR)/loadlib.Po \
+	lua/lua/$(DEPDIR)/lobject.Po lua/lua/$(DEPDIR)/lopcodes.Po \
+	lua/lua/$(DEPDIR)/loslib.Po lua/lua/$(DEPDIR)/lparser.Po \
+	lua/lua/$(DEPDIR)/lstate.Po lua/lua/$(DEPDIR)/lstring.Po \
+	lua/lua/$(DEPDIR)/lstrlib.Po lua/lua/$(DEPDIR)/ltable.Po \
+	lua/lua/$(DEPDIR)/ltablib.Po lua/lua/$(DEPDIR)/ltm.Po \
+	lua/lua/$(DEPDIR)/lundump.Po lua/lua/$(DEPDIR)/lutf8lib.Po \
+	lua/lua/$(DEPDIR)/lvm.Po lua/lua/$(DEPDIR)/lzio.Po \
+	menus/$(DEPDIR)/apropos_menu.Po menus/$(DEPDIR)/bmarks_menu.Po \
 	menus/$(DEPDIR)/cabbrevs_menu.Po \
 	menus/$(DEPDIR)/colorscheme_menu.Po \
 	menus/$(DEPDIR)/commands_menu.Po \
@@ -399,6 +405,7 @@ MANS = $(dist_man_MANS)
 DATA = $(dist_bashcompl_DATA) $(dist_desktopapp_DATA) \
 	$(dist_desktoppix_DATA) $(dist_docs_DATA) \
 	$(dist_fishcompl_DATA) $(dist_global_colors__DATA) \
+	$(dist_hicolor128_DATA) $(dist_hicolorsvg_DATA) \
 	$(dist_pkgdata_DATA) $(dist_sample_colors__DATA) \
 	$(dist_vim_autoload_vifm__DATA) $(dist_vim_doc__DATA) \
 	$(dist_vim_ftdetect__DATA) $(dist_vim_ftplugin__DATA) \
@@ -656,7 +663,12 @@ PACKAGE_URL = @PACKAGE_URL@
 PACKAGE_VERSION = @PACKAGE_VERSION@
 PATH_SEPARATOR = @PATH_SEPARATOR@
 PERL_PROG = @PERL_PROG@
+PTHREAD_CC = @PTHREAD_CC@
+PTHREAD_CFLAGS = @PTHREAD_CFLAGS@
+PTHREAD_CXX = @PTHREAD_CXX@
+PTHREAD_LIBS = @PTHREAD_LIBS@
 SANITIZERS_CFLAGS = @SANITIZERS_CFLAGS@
+SED = @SED_PROG@
 SED_PROG = @SED_PROG@
 SET_MAKE = @SET_MAKE@
 SHELL = @SHELL@
@@ -674,6 +686,7 @@ am__leading_dot = @am__leading_dot@
 am__quote = @am__quote@
 am__tar = @am__tar@
 am__untar = @am__untar@
+ax_pthread_config = @ax_pthread_config@
 bindir = @bindir@
 build = @build@
 build_alias = @build_alias@
@@ -740,7 +753,7 @@ dist_sample_colors__DATA = ../data/colors/astrell-root.vifm \
 									 ../data/colors/Default-256.vifm
 
 dist_vim_doc__DATA = ../data/vim/doc/plugin/vifm-plugin.txt
-nodist_vim_doc__DATA = $(abs_srcdir)/../data/vim/doc/plugin/tags
+nodist_vim_doc__DATA = $(top_srcdir)/data/vim/doc/plugin/tags
 dist_vim_ftdetect__DATA = ../data/vim/ftdetect/vifm.vim \
 										 ../data/vim/ftdetect/vifm-rename.vim
 
@@ -756,7 +769,7 @@ dist_global_colors__DATA = ../data/colors/Default-256.vifm
 dist_vimdoc_doc__DATA = ../data/vim/doc/app/vifm-app.txt \
 												../data/vim/doc/app/vifm-lua.txt
 
-nodist_vimdoc_doc__DATA = $(abs_srcdir)/../data/vim/doc/app/tags
+nodist_vimdoc_doc__DATA = $(top_srcdir)/data/vim/doc/app/tags
 dist_pkgdata_DATA = ../data/vifmrc@DATA_SUFFIX@
 dist_pkgdata_SCRIPTS = ../data/vifm-media@DATA_SUFFIX@
 nodist_pkgdata_DATA = ../data/vifm-help.txt
@@ -768,8 +781,8 @@ dist_man_MANS = ../data/man/vifm.1 \
 
 MOSTLYCLEANFILES = compile_info.c \
 									 ../data/vifm-help.txt \
-									 $(abs_srcdir)/../data/vim/doc/app/tags \
-									 $(abs_srcdir)/../data/vim/doc/plugin/tags
+									 $(top_srcdir)/data/vim/doc/app/tags \
+									 $(top_srcdir)/data/vim/doc/plugin/tags
 
 desktopappdir = $(datadir)/applications
 desktopappfile = ../data/vifm.desktop
@@ -777,6 +790,12 @@ dist_desktopapp_DATA = $(desktopappfile)
 desktoppixdir = $(datadir)/pixmaps
 desktoppixfile = ../data/graphics/vifm.png
 dist_desktoppix_DATA = $(desktoppixfile)
+hicolor128dir = $(datadir)/icons/hicolor/128x128/apps
+hicolor128file = ../data/graphics/vifm.png
+dist_hicolor128_DATA = $(hicolor128file)
+hicolorsvgdir = $(datadir)/icons/hicolor/scalable/apps
+hicolorsvgfile = ../data/graphics/vifm.svg
+dist_hicolorsvg_DATA = $(hicolorsvgfile)
 bashcompldir = $(datadir)/bash-completion/completions
 bashcomplfile = ../data/shell-completion/bash/vifm
 dist_bashcompl_DATA = $(bashcomplfile)
@@ -786,8 +805,8 @@ dist_zshcompl_DATA = $(zshcomplfile)
 fishcompldir = $(datadir)/fish/vendor_completions.d
 fishcomplfile = ../data/shell-completion/fish/vifm.fish
 dist_fishcompl_DATA = $(fishcomplfile)
-docsfile = ../AUTHORS ../BUGS ../COPYING ../ChangeLog ../FAQ ../INSTALL \
-					 ../NEWS ../README ../TODO
+docsfile = ../AUTHORS ../BUGS ../COPYING ../ChangeLog ../ChangeLog.LuaAPI \
+					 ../FAQ ../INSTALL ../NEWS ../README ../TODO
 
 docsdir = $(docdir)
 dist_docs_DATA = $(docsfile)
@@ -882,7 +901,9 @@ vifm_SOURCES = \
 	\
 	lua/api.h \
 	lua/common.c lua/common.h \
+	lua/vifm.c lua/vifm.h \
 	lua/vifm_cmds.c lua/vifm_cmds.h \
+	lua/vifm_events.c lua/vifm_events.h \
 	lua/vifm_handlers.c lua/vifm_handlers.h \
 	lua/vifm_keys.c lua/vifm_keys.h \
 	lua/vifm_tabs.c lua/vifm_tabs.h \
@@ -892,6 +913,7 @@ vifm_SOURCES = \
 	lua/vifmtab.c lua/vifmtab.h \
 	lua/vifmview.c lua/vifmview.h \
 	lua/vlua.c lua/vlua.h \
+	lua/vlua_cbacks.c lua/vlua_cbacks.h \
 	lua/vlua_state.c lua/vlua_state.h \
 	\
 	menus/all.h \
@@ -1066,7 +1088,6 @@ EXTRA_DIST = \
 MANGEN = @MANGEN_PROG@
 COL = @COL_PROG@
 PERL = @PERL_PROG@
-SED = @SED_PROG@
 VIM = @VIM_PROG@
 UNDER_VCS = @IN_GIT_REPO@
 CLEANFILES = runtests lcov-all.info lcov.info $(vifm_OBJECTS:.o=.gcno) \
@@ -1330,8 +1351,11 @@ lua/$(DEPDIR)/$(am__dirstamp):
 	@: > lua/$(DEPDIR)/$(am__dirstamp)
 lua/common.$(OBJEXT): lua/$(am__dirstamp) \
 	lua/$(DEPDIR)/$(am__dirstamp)
+lua/vifm.$(OBJEXT): lua/$(am__dirstamp) lua/$(DEPDIR)/$(am__dirstamp)
 lua/vifm_cmds.$(OBJEXT): lua/$(am__dirstamp) \
 	lua/$(DEPDIR)/$(am__dirstamp)
+lua/vifm_events.$(OBJEXT): lua/$(am__dirstamp) \
+	lua/$(DEPDIR)/$(am__dirstamp)
 lua/vifm_handlers.$(OBJEXT): lua/$(am__dirstamp) \
 	lua/$(DEPDIR)/$(am__dirstamp)
 lua/vifm_keys.$(OBJEXT): lua/$(am__dirstamp) \
@@ -1349,6 +1373,8 @@ lua/vifmtab.$(OBJEXT): lua/$(am__dirstamp) \
 lua/vifmview.$(OBJEXT): lua/$(am__dirstamp) \
 	lua/$(DEPDIR)/$(am__dirstamp)
 lua/vlua.$(OBJEXT): lua/$(am__dirstamp) lua/$(DEPDIR)/$(am__dirstamp)
+lua/vlua_cbacks.$(OBJEXT): lua/$(am__dirstamp) \
+	lua/$(DEPDIR)/$(am__dirstamp)
 lua/vlua_state.$(OBJEXT): lua/$(am__dirstamp) \
 	lua/$(DEPDIR)/$(am__dirstamp)
 menus/$(am__dirstamp):
@@ -1708,7 +1734,9 @@ distclean-compile:
 @AMDEP_TRUE@@am__include@ @am__quote@io/private/$(DEPDIR)/ionotif.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@io/private/$(DEPDIR)/traverser.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@lua/$(DEPDIR)/common.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@lua/$(DEPDIR)/vifm.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@lua/$(DEPDIR)/vifm_cmds.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@lua/$(DEPDIR)/vifm_events.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@lua/$(DEPDIR)/vifm_handlers.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@lua/$(DEPDIR)/vifm_keys.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@lua/$(DEPDIR)/vifm_tabs.Po@am__quote@ # am--include-marker
@@ -1718,6 +1746,7 @@ distclean-compile:
 @AMDEP_TRUE@@am__include@ @am__quote@lua/$(DEPDIR)/vifmtab.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@lua/$(DEPDIR)/vifmview.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@lua/$(DEPDIR)/vlua.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@lua/$(DEPDIR)/vlua_cbacks.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@lua/$(DEPDIR)/vlua_state.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@lua/lua/$(DEPDIR)/lapi.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@lua/lua/$(DEPDIR)/lauxlib.Po@am__quote@ # am--include-marker
@@ -2018,6 +2047,48 @@ uninstall-dist_global_colors_DATA:
 	@list='$(dist_global_colors__DATA)'; test -n "$(global_colors_dir)" || list=; \
 	files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
 	dir='$(DESTDIR)$(global_colors_dir)'; $(am__uninstall_files_from_dir)
+install-dist_hicolor128DATA: $(dist_hicolor128_DATA)
+	@$(NORMAL_INSTALL)
+	@list='$(dist_hicolor128_DATA)'; test -n "$(hicolor128dir)" || list=; \
+	if test -n "$$list"; then \
+	  echo " $(MKDIR_P) '$(DESTDIR)$(hicolor128dir)'"; \
+	  $(MKDIR_P) "$(DESTDIR)$(hicolor128dir)" || exit 1; \
+	fi; \
+	for p in $$list; do \
+	  if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+	  echo "$$d$$p"; \
+	done | $(am__base_list) | \
+	while read files; do \
+	  echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(hicolor128dir)'"; \
+	  $(INSTALL_DATA) $$files "$(DESTDIR)$(hicolor128dir)" || exit $$?; \
+	done
+
+uninstall-dist_hicolor128DATA:
+	@$(NORMAL_UNINSTALL)
+	@list='$(dist_hicolor128_DATA)'; test -n "$(hicolor128dir)" || list=; \
+	files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+	dir='$(DESTDIR)$(hicolor128dir)'; $(am__uninstall_files_from_dir)
+install-dist_hicolorsvgDATA: $(dist_hicolorsvg_DATA)
+	@$(NORMAL_INSTALL)
+	@list='$(dist_hicolorsvg_DATA)'; test -n "$(hicolorsvgdir)" || list=; \
+	if test -n "$$list"; then \
+	  echo " $(MKDIR_P) '$(DESTDIR)$(hicolorsvgdir)'"; \
+	  $(MKDIR_P) "$(DESTDIR)$(hicolorsvgdir)" || exit 1; \
+	fi; \
+	for p in $$list; do \
+	  if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+	  echo "$$d$$p"; \
+	done | $(am__base_list) | \
+	while read files; do \
+	  echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(hicolorsvgdir)'"; \
+	  $(INSTALL_DATA) $$files "$(DESTDIR)$(hicolorsvgdir)" || exit $$?; \
+	done
+
+uninstall-dist_hicolorsvgDATA:
+	@$(NORMAL_UNINSTALL)
+	@list='$(dist_hicolorsvg_DATA)'; test -n "$(hicolorsvgdir)" || list=; \
+	files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+	dir='$(DESTDIR)$(hicolorsvgdir)'; $(am__uninstall_files_from_dir)
 install-dist_pkgdataDATA: $(dist_pkgdata_DATA)
 	@$(NORMAL_INSTALL)
 	@list='$(dist_pkgdata_DATA)'; test -n "$(pkgdatadir)" || list=; \
@@ -2546,7 +2617,7 @@ check-am: all-am
 check: check-am
 all-am: Makefile $(PROGRAMS) $(SCRIPTS) $(MANS) $(DATA)
 installdirs:
-	for dir in "$(DESTDIR)$(bindir)" "$(DESTDIR)$(bindir)" "$(DESTDIR)$(pkgdatadir)" "$(DESTDIR)$(man1dir)" "$(DESTDIR)$(bashcompldir)" "$(DESTDIR)$(desktopappdir)" "$(DESTDIR)$(desktoppixdir)" "$(DESTDIR)$(docsdir)" "$(DESTDIR)$(fishcompldir)" "$(DESTDIR)$(global_colors_dir)" "$(DESTDIR)$(pkgdatadir)" "$(DESTDIR)$(sample_colors_dir)" "$(DESTDIR)$(vim_autoload_vifm_dir)" "$(DESTDIR)$(vim_doc_dir)" "$(DESTDIR)$(vim_ftdetect_dir)" "$(DESTDIR)$(vim_ftplugin_dir)" "$(DESTDIR)$(vim_plugin_dir)" "$(DESTDIR)$(vim_syntax_dir)" "$(DESTDIR)$(vimdoc_doc_dir)" "$(DESTDIR)$(zshcompldir)" "$(DESTDIR)$(pkgdatadir)" "$(DESTDIR)$(vim_doc_dir)" "$(DESTDIR)$(vimdoc_doc_dir)"; do \
+	for dir in "$(DESTDIR)$(bindir)" "$(DESTDIR)$(bindir)" "$(DESTDIR)$(pkgdatadir)" "$(DESTDIR)$(man1dir)" "$(DESTDIR)$(bashcompldir)" "$(DESTDIR)$(desktopappdir)" "$(DESTDIR)$(desktoppixdir)" "$(DESTDIR)$(docsdir)" "$(DESTDIR)$(fishcompldir)" "$(DESTDIR)$(global_colors_dir)" "$(DESTDIR)$(hicolor128dir)" "$(DESTDIR)$(hicolorsvgdir)" "$(DESTDIR)$(pkgdatadir)" "$(DESTDIR)$(sample_colors_dir)" "$(DESTDIR)$(vim_autoload_vifm_dir)" "$(DESTDIR)$(vim_doc_dir)" "$(DESTDIR)$(vim_ftdetect_dir)" "$(DESTDIR)$(vim_ftplugin_dir)" "$(DESTDIR)$(vim_plugin_dir)" "$(DESTDIR)$(vim_syntax_dir)" "$(DESTDIR)$(vimdoc_doc_dir)" "$(DESTDIR)$(zshcompldir)" "$(DESTDIR)$(pkgdatadir)" "$(DESTDIR)$(vim_doc_dir)" "$(DESTDIR)$(vimdoc_doc_dir)"; do \
 	  test -z "$$dir" || $(MKDIR_P) "$$dir"; \
 	done
 install: install-am
@@ -2699,7 +2770,9 @@ distclean: distclean-am
 	-rm -f io/private/$(DEPDIR)/ionotif.Po
 	-rm -f io/private/$(DEPDIR)/traverser.Po
 	-rm -f lua/$(DEPDIR)/common.Po
+	-rm -f lua/$(DEPDIR)/vifm.Po
 	-rm -f lua/$(DEPDIR)/vifm_cmds.Po
+	-rm -f lua/$(DEPDIR)/vifm_events.Po
 	-rm -f lua/$(DEPDIR)/vifm_handlers.Po
 	-rm -f lua/$(DEPDIR)/vifm_keys.Po
 	-rm -f lua/$(DEPDIR)/vifm_tabs.Po
@@ -2709,6 +2782,7 @@ distclean: distclean-am
 	-rm -f lua/$(DEPDIR)/vifmtab.Po
 	-rm -f lua/$(DEPDIR)/vifmview.Po
 	-rm -f lua/$(DEPDIR)/vlua.Po
+	-rm -f lua/$(DEPDIR)/vlua_cbacks.Po
 	-rm -f lua/$(DEPDIR)/vlua_state.Po
 	-rm -f lua/lua/$(DEPDIR)/lapi.Po
 	-rm -f lua/lua/$(DEPDIR)/lauxlib.Po
@@ -2837,7 +2911,8 @@ info-am:
 install-data-am: install-dist_bashcomplDATA \
 	install-dist_desktopappDATA install-dist_desktoppixDATA \
 	install-dist_docsDATA install-dist_fishcomplDATA \
-	install-dist_global_colors_DATA install-dist_pkgdataDATA \
+	install-dist_global_colors_DATA install-dist_hicolor128DATA \
+	install-dist_hicolorsvgDATA install-dist_pkgdataDATA \
 	install-dist_pkgdataSCRIPTS install-dist_sample_colors_DATA \
 	install-dist_vim_autoload_vifm_DATA install-dist_vim_doc_DATA \
 	install-dist_vim_ftdetect_DATA install-dist_vim_ftplugin_DATA \
@@ -2957,7 +3032,9 @@ maintainer-clean: maintainer-clean-am
 	-rm -f io/private/$(DEPDIR)/ionotif.Po
 	-rm -f io/private/$(DEPDIR)/traverser.Po
 	-rm -f lua/$(DEPDIR)/common.Po
+	-rm -f lua/$(DEPDIR)/vifm.Po
 	-rm -f lua/$(DEPDIR)/vifm_cmds.Po
+	-rm -f lua/$(DEPDIR)/vifm_events.Po
 	-rm -f lua/$(DEPDIR)/vifm_handlers.Po
 	-rm -f lua/$(DEPDIR)/vifm_keys.Po
 	-rm -f lua/$(DEPDIR)/vifm_tabs.Po
@@ -2967,6 +3044,7 @@ maintainer-clean: maintainer-clean-am
 	-rm -f lua/$(DEPDIR)/vifmtab.Po
 	-rm -f lua/$(DEPDIR)/vifmview.Po
 	-rm -f lua/$(DEPDIR)/vlua.Po
+	-rm -f lua/$(DEPDIR)/vlua_cbacks.Po
 	-rm -f lua/$(DEPDIR)/vlua_state.Po
 	-rm -f lua/lua/$(DEPDIR)/lapi.Po
 	-rm -f lua/lua/$(DEPDIR)/lauxlib.Po
@@ -3095,6 +3173,7 @@ uninstall-am: uninstall-binPROGRAMS uninstall-dist_bashcomplDATA \
 	uninstall-dist_binSCRIPTS uninstall-dist_desktopappDATA \
 	uninstall-dist_desktoppixDATA uninstall-dist_docsDATA \
 	uninstall-dist_fishcomplDATA uninstall-dist_global_colors_DATA \
+	uninstall-dist_hicolor128DATA uninstall-dist_hicolorsvgDATA \
 	uninstall-dist_pkgdataDATA uninstall-dist_pkgdataSCRIPTS \
 	uninstall-dist_sample_colors_DATA \
 	uninstall-dist_vim_autoload_vifm_DATA \
@@ -3118,7 +3197,8 @@ uninstall-man: uninstall-man1
 	install-dist_bashcomplDATA install-dist_binSCRIPTS \
 	install-dist_desktopappDATA install-dist_desktoppixDATA \
 	install-dist_docsDATA install-dist_fishcomplDATA \
-	install-dist_global_colors_DATA install-dist_pkgdataDATA \
+	install-dist_global_colors_DATA install-dist_hicolor128DATA \
+	install-dist_hicolorsvgDATA install-dist_pkgdataDATA \
 	install-dist_pkgdataSCRIPTS install-dist_sample_colors_DATA \
 	install-dist_vim_autoload_vifm_DATA install-dist_vim_doc_DATA \
 	install-dist_vim_ftdetect_DATA install-dist_vim_ftplugin_DATA \
@@ -3137,6 +3217,7 @@ uninstall-man: uninstall-man1
 	uninstall-dist_binSCRIPTS uninstall-dist_desktopappDATA \
 	uninstall-dist_desktoppixDATA uninstall-dist_docsDATA \
 	uninstall-dist_fishcomplDATA uninstall-dist_global_colors_DATA \
+	uninstall-dist_hicolor128DATA uninstall-dist_hicolorsvgDATA \
 	uninstall-dist_pkgdataDATA uninstall-dist_pkgdataSCRIPTS \
 	uninstall-dist_sample_colors_DATA \
 	uninstall-dist_vim_autoload_vifm_DATA \
@@ -3152,74 +3233,76 @@ uninstall-man: uninstall-man1
 
 .PHONY: update_compile_info
 clean-local:
-	if [ '$(abs_builddir)' != '$(abs_srcdir)' ]; then \
+	if [ '$(builddir)' != '$(srcdir)' ]; then \
 		$(RM) tags.c; \
 	fi
 
 runtests:
-	echo 'mkdir -p $(abs_builddir)/../tests/' > $@_
+	$(AM_V_GEN)echo 'mkdir -p "$(abs_top_builddir)/tests/"' > $@_ && \
 	echo \
-		'$(MAKE) -C $(abs_srcdir)/../tests B=$(abs_builddir)/../tests/ CC="$(CC)"' \
-		>> $@_
-	chmod +x $@_
+		'$(MAKE) -C "$(abs_top_srcdir)/tests" B="$(abs_top_builddir)/tests/" CC="$(CC)" "$$@"' \
+		>> $@_ && \
+	chmod +x $@_ && \
 	mv $@_ $@
 
 coverage: check
-	lcov --directory $(abs_builddir)/ --base-directory . \
-	     --capture --output-file $(abs_builddir)/lcov-all.info --config-file \
-	     $(abs_srcdir)/lcovrc --test-name unit_tests --quiet
-	lcov --remove $(abs_builddir)/lcov-all.info -o $(abs_builddir)/lcov.info \
+	lcov --directory $(builddir)/ --base-directory . \
+	     --capture --output-file $(builddir)/lcov-all.info --config-file \
+	     $(srcdir)/lcovrc --test-name unit_tests --quiet
+	lcov --remove $(builddir)/lcov-all.info -o $(builddir)/lcov.info \
 	     '/usr/*'
-	genhtml --output-directory $(abs_builddir)/cov-report/ \
-	     $(abs_builddir)/lcov.info --config-file $(abs_srcdir)/lcovrc \
-	     --show-details --css-file $(abs_srcdir)/lcov.css
+	genhtml --output-directory $(builddir)/cov-report/ \
+	     $(builddir)/lcov.info --config-file $(srcdir)/lcovrc \
+	     --show-details --css-file $(srcdir)/lcov.css
 
 distclean-local:
-	$(MAKE) -C $(abs_srcdir)/../tests B=$(abs_builddir)/../tests/ clean
+	$(MAKE) -C "$(abs_top_srcdir)/tests" B="$(abs_top_builddir)/tests/" clean
 	$(RM) -r cov-report/
 
-version.o: $(filter-out version.o, $(vifm_OBJECTS))
+version.o: $(vifm_OBJECTS:version.o=)
 compile_info.c: update_compile_info
-	@$(abs_srcdir)/update-compile-info $(UNDER_VCS)
+	@$(srcdir)/update-compile-info $(UNDER_VCS)
 
 # No action needed for this target.
 update_compile_info:
 
-../data/vifm-help.txt: $(abs_srcdir)/../data/man/vifm.1
+../data/vifm-help.txt: $(top_srcdir)/data/man/vifm.1
 	$(AM_V_GEN)mkdir -p ../data/; \
 	if [ -n "$(MANGEN)" -a -n "$(SED)" ]; then \
 		if [ -n "$(COL)" ]; then \
-			$(MANGEN) -Tascii -man $(abs_srcdir)/../data/man/vifm.1 | $(SED) -e 's/\x1b\[[0-9]*m//g' -e 's/\x0d//g' | $(COL) -b >| '$@'; \
+			$(MANGEN) -Tascii -man $(top_srcdir)/data/man/vifm.1 | $(SED) -e 's/\x1b\[[0-9]*m//g' -e 's/\x0d//g' | $(COL) -b >| '$@'; \
 		else \
-			$(MANGEN) -Tascii -man $(abs_srcdir)/../data/man/vifm.1 | $(SED) -e 's/.\x08//g' -e 's/\x1b\[[0-9]*m//g' -e 's/\x0d//g' >| '$@'; \
+			$(MANGEN) -Tascii -man $(top_srcdir)/data/man/vifm.1 | $(SED) -e 's/.\x08//g' -e 's/\x1b\[[0-9]*m//g' -e 's/\x0d//g' >| '$@'; \
 		fi \
 	fi
 
-$(abs_srcdir)/../data/vim/doc/app/tags: \
-                                  $(abs_srcdir)/../data/vim/doc/app/vifm-app.txt \
-                                  $(abs_srcdir)/../data/vim/doc/app/vifm-lua.txt
+$(top_srcdir)/data/vim/doc/app/tags: \
+                                   $(top_srcdir)/data/vim/doc/app/vifm-app.txt \
+                                   $(top_srcdir)/data/vim/doc/app/vifm-lua.txt
 	$(AM_V_GEN)mkdir -p ../data/vim/doc/app/; \
 	if [ -n "$(PERL)" ]; then \
-		$(abs_srcdir)/helpztags "$(abs_srcdir)/../data/vim/doc/app"; \
+		$(srcdir)/helpztags "$(top_srcdir)/data/vim/doc/app"; \
 	elif [ -n "$(VIM)" ]; then \
-		vim -e -s -c 'helptags $(abs_srcdir)/../data/vim/doc/app|q'; \
+		vim -e -s -c 'helptags $(top_srcdir)/data/vim/doc/app|q'; \
 	else \
-		touch $@; \
+		echo "Can't generate tags without perl or vim"; \
+		false; \
 	fi
 
-$(abs_srcdir)/../data/vim/doc/plugin/tags: \
-                            $(abs_srcdir)/../data/vim/doc/plugin/vifm-plugin.txt
+$(top_srcdir)/data/vim/doc/plugin/tags: \
+                               $(top_srcdir)/data/vim/doc/plugin/vifm-plugin.txt
 	$(AM_V_GEN)mkdir -p ../data/vim/doc/plugin/; \
 	if [ -n "$(PERL)" ]; then \
-		$(abs_srcdir)/helpztags "$(abs_srcdir)/../data/vim/doc/plugin"; \
+		$(srcdir)/helpztags "$(top_srcdir)/data/vim/doc/plugin"; \
 	elif [ -n "$(VIM)" ]; then \
-		vim -e -s -c 'helptags $(abs_srcdir)/../data/vim/doc/plugin|q'; \
+		vim -e -s -c 'helptags $(top_srcdir)/data/vim/doc/plugin|q'; \
 	else \
-		touch $@; \
+		echo "Can't generate tags without perl or vim"; \
+		false; \
 	fi
 
-tags.c: $(abs_srcdir)/../data/vim/doc/app/tags
-	$(AM_V_GEN)if [ -s $(abs_srcdir)/../data/vim/doc/app/tags -a -n "$(AWK)" ]; then \
+tags.c: $(top_srcdir)/data/vim/doc/app/tags
+	$(AM_V_GEN)if [ -s $(top_srcdir)/data/vim/doc/app/tags -a -n "$(AWK)" ]; then \
 		$(AWK) ' \
 			BEGIN { \
 				print "const char *tags[] = {" \
@@ -3230,8 +3313,8 @@ tags.c: $(abs_srcdir)/../data/vim/doc/app/tags
 			END { \
 				print "\t0,\n};" \
 			} \
-		' $(abs_srcdir)/../data/vim/doc/app/tags > $@; \
-	elif [ '$(abs_builddir)' != '$(abs_srcdir)' ]; then \
+		' $(top_srcdir)/data/vim/doc/app/tags > $@; \
+	elif [ '$(builddir)' != '$(srcdir)' ]; then \
 		echo 'const char *tags[] = {};' > $@; \
 	fi
 
diff --git a/src/Makefile.win b/src/Makefile.win
index 5883492..18bccb4 100644
--- a/src/Makefile.win
+++ b/src/Makefile.win
@@ -56,10 +56,11 @@ lua := lapi.c lauxlib.c lbaselib.c lcode.c lcorolib.c lctype.c ldblib.c \
        lstring.c lstrlib.c ltable.c ltablib.c ltm.c lundump.c lutf8lib.c lvm.c \
        lzio.c
 lua := $(addprefix lua/, $(lua))
-lua := $(addprefix lua/, $(lua) common.c vifm_cmds.c vifm_handlers.c \
-                                vlua_state.c vifm_keys.c vifm_tabs.c \
+lua := $(addprefix lua/, $(lua) common.c vifm.c vifm_cmds.c vifm_events.c \
+                                vifm_handlers.c vifm_keys.c vifm_tabs.c \
                                 vifm_viewcolumns.c vifmentry.c vifmjob.c \
-                                vifmtab.c vifmview.c vlua.c)
+                                vifmtab.c vifmview.c vlua.c vlua_cbacks.c \
+                                vlua_state.c)
 
 menus := apropos_menu.c bmarks_menu.c cabbrevs_menu.c colorscheme_menu.c \
          commands_menu.c dirhistory_menu.c dirstack_menu.c filetypes_menu.c \
@@ -137,7 +138,7 @@ vifmres.o: vifm.res
 ../build-aux/config.h: Makefile.win
 	@echo Creating sample $@
 	mkdir -p ../build-aux
-	echo '#define VERSION "0.12.1"' > $@; \
+	echo '#define VERSION "0.13"' > $@; \
 	echo '#define ENABLE_EXTENDED_KEYS' >> $@; \
 	echo '#define _GNU_SOURCE 1' >> $@; \
 	echo '#define _XOPEN_SOURCE 1' >> $@; \
diff --git a/src/args.c b/src/args.c
index 228dcc5..688b927 100644
--- a/src/args.c
+++ b/src/args.c
@@ -61,6 +61,7 @@ static struct option long_opts[] = {
 	{ "choose-dir",   required_argument, .flag = NULL, .val = 'D' },
 	{ "delimiter",    required_argument, .flag = NULL, .val = 'd' },
 	{ "on-choose",    required_argument, .flag = NULL, .val = 'o' },
+	{ "plugins-dir",  required_argument, .flag = NULL, .val = 'p' },
 
 #ifdef ENABLE_REMOTE_CMDS
 	{ "server-list",  no_argument,       .flag = NULL, .val = 'L' },
@@ -88,6 +89,8 @@ args_parse(args_t *args, int argc, char *argv[], const char dir[])
 	{
 		switch(getopt_long(argc, argv, "-c:fhv", long_opts, NULL))
 		{
+			char path_buf[PATH_MAX + 1];
+
 			case 'f': /* -f */
 				args->file_picker = 1;
 				break;
@@ -137,6 +140,11 @@ args_parse(args_t *args, int argc, char *argv[], const char dir[])
 			case 'c': /* -c <cmd> */
 				args->ncmds = add_to_string_array(&args->cmds, args->ncmds, optarg);
 				break;
+			case 'p': /* --plugins-dir <path> */
+				parse_path(dir, optarg, path_buf);
+				args->nplugins_dirs = add_to_string_array(&args->plugins_dirs,
+						args->nplugins_dirs, path_buf);
+				break;
 			case 'l': /* --logging */
 				args->logging = 1;
 				if(!is_null_or_empty(optarg))
@@ -412,6 +420,8 @@ show_help_msg(const char wrong_arg[])
 	puts("  vifm --on-choose <command>");
 	puts("    sets command to be executed on selected files instead of opening");
 	puts("    them.  Command can use any of command macros.\n");
+	puts("  vifm --plugins-dir <path>");
+	puts("    additional plugins directory (can appear multiple times).\n");
 	puts("  vifm --logging[=<startup log path>]");
 	puts("    log some operational details to $XDG_DATA_HOME/vifm/log or");
 	puts("    $VIFM/log.  If the optional startup log path is specified and");
@@ -523,6 +533,20 @@ process_other_args(args_t *args)
 	{
 		stats_set_on_choose(args->on_choose);
 	}
+
+	free_string_array(curr_stats.plugins_dirs.items,
+			curr_stats.plugins_dirs.nitems);
+	curr_stats.plugins_dirs.items = NULL;
+	curr_stats.plugins_dirs.nitems = 0;
+
+	/* Adding in reversed order. */
+	int i;
+	for(i = (int)args->nplugins_dirs - 1; i >= 0; --i)
+	{
+		curr_stats.plugins_dirs.nitems = add_to_string_array(
+				&curr_stats.plugins_dirs.items, curr_stats.plugins_dirs.nitems,
+				args->plugins_dirs[i]);
+	}
 }
 
 /* Quits during argument parsing when it's allowed (e.g. not for remote
@@ -545,6 +569,10 @@ args_free(args_t *args)
 		args->cmds = NULL;
 		args->ncmds = 0;
 
+		free_string_array(args->plugins_dirs, args->nplugins_dirs);
+		args->plugins_dirs = NULL;
+		args->nplugins_dirs = 0;
+
 		update_string(&args->startup_log_path, NULL);
 	}
 }
diff --git a/src/args.h b/src/args.h
index 64d7fef..67a488f 100644
--- a/src/args.h
+++ b/src/args.h
@@ -23,6 +23,7 @@
 #include <stddef.h> /* size_t */
 
 #include "compat/fs_limits.h"
+#include "utils/string_array.h"
 
 /* Set of arguments that are processed at the same time. */
 typedef enum
@@ -63,6 +64,9 @@ typedef struct
 
 	char **cmds;  /* List of startup commands. */
 	size_t ncmds; /* Number of startup commands. */
+
+	char **plugins_dirs;  /* List of additional plugin search paths. */
+	size_t nplugins_dirs; /* Their count. */
 }
 args_t;
 
diff --git a/src/background.c b/src/background.c
index 2bfbc99..67b1013 100644
--- a/src/background.c
+++ b/src/background.c
@@ -133,8 +133,6 @@ static void get_off_job_bar(bg_job_t *job);
 static bg_job_t * add_background_job(pid_t pid, const char cmd[],
 		uintptr_t err, uintptr_t data, BgJobType type, int with_bg_op);
 static void * background_task_bootstrap(void *arg);
-static void set_current_job(bg_job_t *job);
-static void make_current_job_key(void);
 static int update_job_status(bg_job_t *job);
 static void mark_job_finished(bg_job_t *job, int exit_code);
 static int bg_op_cancel(bg_op_t *bg_op);
@@ -148,19 +146,25 @@ static pthread_mutex_t new_err_jobs_lock = PTHREAD_MUTEX_INITIALIZER;
 /* Conditional variable to signal availability of new jobs in new_err_jobs. */
 static pthread_cond_t new_err_jobs_cond = PTHREAD_COND_INITIALIZER;
 
-/* Thread local storage for bg_job_t associated with active thread. */
+/* Thread-local storage for bg_job_t associated with active thread. */
 static pthread_key_t current_job;
 
-void
+int
 bg_init(void)
 {
+	/* Create thread-local storage before starting any background threads. */
+	if(pthread_key_create(&current_job, NULL) != 0)
+	{
+		return 1;
+	}
+
 	pthread_t id;
-	int err = pthread_create(&id, NULL, &error_thread, NULL);
-	assert(err == 0);
-	(void)err;
+	if(pthread_create(&id, NULL, &error_thread, NULL) != 0)
+	{
+		return 1;
+	}
 
-	/* Initialize state for the main thread. */
-	set_current_job(NULL);
+	return 0;
 }
 
 void
@@ -189,16 +193,30 @@ bg_check(void)
 	{
 		job_check(p);
 
-		pthread_spin_lock(&p->status_lock);
-		int running = p->running;
-		int can_remove = (!p->running && p->use_count == 0);
-		pthread_spin_unlock(&p->status_lock);
+		/* In case of lock failure, assume the job is active. */
+		int running = 1;
+		int can_remove = 0;
 
-		active_jobs += (running != 0 && p->in_menu);
+		if(pthread_spin_lock(&p->status_lock) == 0)
+		{
+			running = p->running;
+			can_remove = (!running && p->use_count == 0);
+			(void)pthread_spin_unlock(&p->status_lock);
+		}
 
-		if(!running && p->on_job_bar)
+		active_jobs += (running && p->in_menu);
+
+		if(!running)
 		{
-			get_off_job_bar(p);
+			if(p->on_job_bar)
+			{
+				get_off_job_bar(p);
+			}
+			if(p->exit_cb != NULL)
+			{
+				p->exit_cb(p, p->exit_cb_arg);
+				p->exit_cb = NULL;
+			}
 		}
 
 		/* Remove job if it is finished now. */
@@ -253,11 +271,14 @@ job_check(bg_job_t *job)
 	/* Display portions of errors from the job while there are any. */
 	do
 	{
-		pthread_spin_lock(&job->errors_lock);
-		new_errors = job->new_errors;
-		job->new_errors = NULL;
-		job->new_errors_len = 0U;
-		pthread_spin_unlock(&job->errors_lock);
+		new_errors = NULL;
+		if(pthread_spin_lock(&job->errors_lock) == 0)
+		{
+			new_errors = job->new_errors;
+			job->new_errors = NULL;
+			job->new_errors_len = 0U;
+			(void)pthread_spin_unlock(&job->errors_lock);
+		}
 
 		if(new_errors != NULL && !job->skip_errors)
 		{
@@ -377,7 +398,7 @@ bg_and_wait_for_errors(char cmd[], const struct cancellation_t *cancellation)
 		}
 		else
 		{
-			result = status_to_exit_code(get_proc_exit_status(pid));
+			result = status_to_exit_code(get_proc_exit_status(pid, cancellation));
 		}
 	}
 
@@ -460,11 +481,10 @@ error_thread(void *p)
 				job = &j->err_next;
 			}
 
-			if(!need_update_list)
+			if(!need_update_list && pthread_mutex_lock(&new_err_jobs_lock) == 0)
 			{
-				pthread_mutex_lock(&new_err_jobs_lock);
 				need_update_list = (new_err_jobs != NULL);
-				pthread_mutex_unlock(&new_err_jobs_lock);
+				(void)pthread_mutex_unlock(&new_err_jobs_lock);
 			}
 			if(need_update_list)
 			{
@@ -494,18 +514,17 @@ free_drained_jobs(bg_job_t **jobs)
 	{
 		bg_job_t *const j = *job;
 
-		if(j->drained)
+		if(j->drained && pthread_spin_lock(&j->status_lock) == 0)
 		{
-			pthread_spin_lock(&j->status_lock);
 			/* If finished, decrement use_count and drop it from the list. */
 			if(!j->running)
 			{
 				--j->use_count;
 				*job = j->err_next;
-				pthread_spin_unlock(&j->status_lock);
+				(void)pthread_spin_unlock(&j->status_lock);
 				continue;
 			}
-			pthread_spin_unlock(&j->status_lock);
+			(void)pthread_spin_unlock(&j->status_lock);
 		}
 
 		job = &j->err_next;
@@ -519,14 +538,20 @@ import_error_jobs(bg_job_t **jobs)
 	bg_job_t *new_jobs;
 
 	/* Add new tasks to internal list, wait if there are no jobs. */
-	pthread_mutex_lock(&new_err_jobs_lock);
+	if(pthread_mutex_lock(&new_err_jobs_lock) != 0)
+	{
+		return;
+	}
 	while(*jobs == NULL && new_err_jobs == NULL)
 	{
-		pthread_cond_wait(&new_err_jobs_cond, &new_err_jobs_lock);
+		if(pthread_cond_wait(&new_err_jobs_cond, &new_err_jobs_lock) != 0)
+		{
+			break;
+		}
 	}
 	new_jobs = new_err_jobs;
 	new_err_jobs = NULL;
-	pthread_mutex_unlock(&new_err_jobs_lock);
+	(void)pthread_mutex_unlock(&new_err_jobs_lock);
 
 	/* Prepend new jobs to the list. */
 	while(new_jobs != NULL)
@@ -617,12 +642,11 @@ report_error_msg(const char title[], const char text[])
 static void
 append_error_msg(bg_job_t *job, const char err_msg[])
 {
-	if(err_msg[0] != '\0')
+	if(err_msg[0] != '\0' && pthread_spin_lock(&job->errors_lock) == 0)
 	{
-		pthread_spin_lock(&job->errors_lock);
 		(void)strappend(&job->errors, &job->errors_len, err_msg);
 		(void)strappend(&job->new_errors, &job->new_errors_len, err_msg);
-		pthread_spin_unlock(&job->errors_lock);
+		(void)pthread_spin_unlock(&job->errors_lock);
 	}
 }
 
@@ -634,7 +658,7 @@ bg_run_and_capture(char cmd[], int user_sh, FILE *in, FILE **out, FILE **err)
 	int out_pipe[2];
 	int error_pipe[2];
 
-	if(pipe(out_pipe) != 0)
+	if(out != NULL && pipe(out_pipe) != 0)
 	{
 		show_error_msg("File pipe error", "Error creating pipe");
 		return (pid_t)-1;
@@ -643,8 +667,11 @@ bg_run_and_capture(char cmd[], int user_sh, FILE *in, FILE **out, FILE **err)
 	if(pipe(error_pipe) != 0)
 	{
 		show_error_msg("File pipe error", "Error creating pipe");
-		close(out_pipe[0]);
-		close(out_pipe[1]);
+		if(out != NULL)
+		{
+			close(out_pipe[0]);
+			close(out_pipe[1]);
+		}
 		return (pid_t)-1;
 	}
 
@@ -655,8 +682,11 @@ bg_run_and_capture(char cmd[], int user_sh, FILE *in, FILE **out, FILE **err)
 
 	if((pid = fork()) == -1)
 	{
-		close(out_pipe[0]);
-		close(out_pipe[1]);
+		if(out != NULL)
+		{
+			close(out_pipe[0]);
+			close(out_pipe[1]);
+		}
 		close(error_pipe[0]);
 		close(error_pipe[1]);
 		return (pid_t)-1;
@@ -664,20 +694,13 @@ bg_run_and_capture(char cmd[], int user_sh, FILE *in, FILE **out, FILE **err)
 
 	if(pid == 0)
 	{
-		char *sh;
-		char *sh_flag;
-
-		close(out_pipe[0]);
-		close(error_pipe[0]);
-		if(dup2(out_pipe[1], STDOUT_FILENO) == -1)
+		if(out != NULL)
 		{
-			_Exit(EXIT_FAILURE);
-		}
-		if(dup2(error_pipe[1], STDERR_FILENO) == -1)
-		{
-			_Exit(EXIT_FAILURE);
+			bind_pipe_or_die(STDOUT_FILENO, out_pipe[1], out_pipe[0]);
 		}
 
+		bind_pipe_or_die(STDERR_FILENO, error_pipe[1], error_pipe[0]);
+
 		if(in != NULL)
 		{
 			rewind(in);
@@ -690,25 +713,29 @@ bg_run_and_capture(char cmd[], int user_sh, FILE *in, FILE **out, FILE **err)
 			fclose(in);
 		}
 
-		sh = user_sh ? get_execv_path(cfg.shell) : "/bin/sh";
-		sh_flag = user_sh ? cfg.shell_cmd_flag : "-c";
+		char *sh_flag = user_sh ? cfg.shell_cmd_flag : "-c";
 		prepare_for_exec();
-		execvp(sh, make_execv_array(sh, sh_flag, cmd));
+		execvp(get_execv_path(cfg.shell),
+				make_execv_array(cfg.shell, sh_flag, cmd));
 		_Exit(127);
 	}
 
-	close(out_pipe[1]);
+	if(out != NULL)
+	{
+		close(out_pipe[1]);
+		*out = fdopen(out_pipe[0], "r");
+	}
+
 	close(error_pipe[1]);
-	*out = fdopen(out_pipe[0], "r");
 	*err = fdopen(error_pipe[0], "r");
 
 	return pid;
 }
 #else
 /* Runs command in a background and redirects its stdout and stderr streams to
- * file streams which are set.  Input is redirected only if in parameter isn't
- * NULL.  Don't pass pipe for input, it can cause deadlock.  Returns (pid_t)0 or
- * (pid_t)-1 on error. */
+ * file streams which are set.  Input and output are redirected only if the
+ * corresponding parameter isn't NULL.  Don't pass pipe for input, it can cause
+ * deadlock.  Returns (pid_t)0 or (pid_t)-1 on error. */
 static pid_t
 background_and_capture_internal(char cmd[], int user_sh, FILE *in, FILE **out,
 		FILE **err, int out_pipe[2], int err_pipe[2])
@@ -730,7 +757,7 @@ background_and_capture_internal(char cmd[], int user_sh, FILE *in, FILE **out,
 			return (pid_t)-1;
 	}
 
-	if(_dup2(out_pipe[1], _fileno(stdout)) != 0)
+	if(out != NULL && _dup2(out_pipe[1], _fileno(stdout)) != 0)
 		return (pid_t)-1;
 	if(_dup2(err_pipe[1], _fileno(stderr)) != 0)
 		return (pid_t)-1;
@@ -781,11 +808,17 @@ background_and_capture_internal(char cmd[], int user_sh, FILE *in, FILE **out,
 		return (pid_t)-1;
 	}
 
-	if((*out = _fdopen(out_pipe[0], "r")) == NULL)
+	if(out != NULL && (*out = _fdopen(out_pipe[0], "r")) == NULL)
+	{
 		return (pid_t)-1;
+	}
+
 	if((*err = _fdopen(err_pipe[0], "r")) == NULL)
 	{
-		fclose(*out);
+		if(out != NULL)
+		{
+			fclose(*out);
+		}
 		return (pid_t)-1;
 	}
 
@@ -800,7 +833,7 @@ bg_run_and_capture(char cmd[], int user_sh, FILE *in, FILE **out, FILE **err)
 	int err_fd, err_pipe[2];
 	pid_t pid;
 
-	if(_pipe(out_pipe, 512, O_NOINHERIT) != 0)
+	if(out != NULL && _pipe(out_pipe, 512, O_NOINHERIT) != 0)
 	{
 		show_error_msg("File pipe error", "Error creating pipe");
 		return (pid_t)-1;
@@ -809,8 +842,11 @@ bg_run_and_capture(char cmd[], int user_sh, FILE *in, FILE **out, FILE **err)
 	if(_pipe(err_pipe, 512, O_NOINHERIT) != 0)
 	{
 		show_error_msg("File pipe error", "Error creating pipe");
-		close(out_pipe[0]);
-		close(out_pipe[1]);
+		if(out != NULL)
+		{
+			close(out_pipe[0]);
+			close(out_pipe[1]);
+		}
 		return (pid_t)-1;
 	}
 
@@ -821,7 +857,10 @@ bg_run_and_capture(char cmd[], int user_sh, FILE *in, FILE **out, FILE **err)
 	pid = background_and_capture_internal(cmd, user_sh, in, out, err, out_pipe,
 			err_pipe);
 
-	_close(out_pipe[1]);
+	if(out != NULL)
+	{
+		_close(out_pipe[1]);
+	}
 	_close(err_pipe[1]);
 
 	_dup2(in_fd, _fileno(stdin));
@@ -830,7 +869,10 @@ bg_run_and_capture(char cmd[], int user_sh, FILE *in, FILE **out, FILE **err)
 
 	if(pid == (pid_t)-1)
 	{
-		_close(out_pipe[0]);
+		if(out != NULL)
+		{
+			_close(out_pipe[0]);
+		}
 		_close(err_pipe[0]);
 	}
 
@@ -908,7 +950,7 @@ launch_external(const char cmd[], BgJobFlags flags, ShellRequester by)
 	const int merge_streams = (capture_output && (flags & BJF_MERGE_STREAMS));
 
 #ifndef _WIN32
-	const int keep_session = (flags & BJF_KEEP_SESSION);
+	const int keep_in_fg = (flags & BJF_KEEP_IN_FG);
 
 	pid_t pid;
 	int input_pipe[2] = { -1, -1 };
@@ -977,32 +1019,22 @@ launch_external(const char cmd[], BgJobFlags flags, ShellRequester by)
 		}
 		close(STDIN_FILENO);
 		close(STDOUT_FILENO);
-		/* Close read end of pipe. */
+
+		/* Close original error pipe descriptors. */
 		if(error_pipe[0] != -1)
 		{
 			close(error_pipe[0]);
+			close(error_pipe[1]);
 		}
 
 		if(supply_input)
 		{
-			if(dup2(input_pipe[0], STDIN_FILENO) == -1)
-			{
-				perror("dup2");
-				_Exit(EXIT_FAILURE);
-			}
-			/* Close write end of pipe. */
-			close(input_pipe[1]);
+			bind_pipe_or_die(STDIN_FILENO, input_pipe[0], input_pipe[1]);
 		}
 
 		if(capture_output)
 		{
-			if(dup2(output_pipe[1], STDOUT_FILENO) == -1)
-			{
-				perror("dup2");
-				_Exit(EXIT_FAILURE);
-			}
-			/* Close read end of pipe. */
-			close(output_pipe[0]);
+			bind_pipe_or_die(STDOUT_FILENO, output_pipe[1], output_pipe[0]);
 		}
 
 		/* Attach stdin and optionally stdout to /dev/null. */
@@ -1019,19 +1051,15 @@ launch_external(const char cmd[], BgJobFlags flags, ShellRequester by)
 				perror("dup2 for stdout");
 				_Exit(EXIT_FAILURE);
 			}
-		}
-
-		if(keep_session)
-		{
-			if(setpgid(0, 0) == -1)
+			if(nullfd != STDIN_FILENO && nullfd != STDOUT_FILENO)
 			{
-				perror("setpgid");
-				_Exit(EXIT_FAILURE);
+				close(nullfd);
 			}
 		}
+
 		/* setsid() creates process group as well and doesn't work if current
 		 * process is a group leader, so don't do setpgid(). */
-		else if(setsid() == (pid_t)-1)
+		if(!keep_in_fg && setsid() == (pid_t)-1)
 		{
 			perror("setsid");
 			_Exit(EXIT_FAILURE);
@@ -1250,10 +1278,12 @@ bg_execute(const char descr[], const char op_descr[], int total, int important,
 	if(pthread_create(&id, NULL, &background_task_bootstrap, task_args) != 0)
 	{
 		/* Mark job as finished with error. */
-		pthread_spin_lock(&task_args->job->status_lock);
-		task_args->job->running = 0;
-		task_args->job->exit_code = 1;
-		pthread_spin_unlock(&task_args->job->status_lock);
+		if(pthread_spin_lock(&task_args->job->status_lock) == 0)
+		{
+			task_args->job->running = 0;
+			task_args->job->exit_code = 1;
+			(void)pthread_spin_unlock(&task_args->job->status_lock);
+		}
 
 		free(task_args);
 		ret = 1;
@@ -1291,12 +1321,15 @@ add_background_job(pid_t pid, const char cmd[], uintptr_t err, uintptr_t data,
 	bg_job_t *new = malloc(sizeof(*new));
 	if(new == NULL)
 	{
-		show_error_msg("Memory error", "Unable to allocate enough memory");
 		return NULL;
 	}
+	new->cmd = strdup(cmd);
+	if(new->cmd == NULL)
+	{
+		goto free_new;
+	}
 	new->type = type;
 	new->pid = pid;
-	new->cmd = strdup(cmd);
 	new->next = bg_jobs;
 	new->skip_errors = 0;
 	new->new_errors = NULL;
@@ -1305,8 +1338,20 @@ add_background_job(pid_t pid, const char cmd[], uintptr_t err, uintptr_t data,
 	new->errors_len = 0U;
 	new->cancelled = 0;
 
-	pthread_spin_init(&new->errors_lock, PTHREAD_PROCESS_PRIVATE);
-	pthread_spin_init(&new->status_lock, PTHREAD_PROCESS_PRIVATE);
+	if(pthread_spin_init(&new->errors_lock, PTHREAD_PROCESS_PRIVATE) != 0)
+	{
+		goto free_cmd;
+	}
+	if(pthread_spin_init(&new->status_lock, PTHREAD_PROCESS_PRIVATE) != 0)
+	{
+		goto free_errors_lock;
+	}
+	if(with_bg_op &&
+			pthread_spin_init(&new->bg_op_lock, PTHREAD_PROCESS_PRIVATE) != 0)
+	{
+		goto free_status_lock;
+	}
+
 	new->running = 1;
 	new->use_count = 0;
 	new->exit_code = -1;
@@ -1314,6 +1359,9 @@ add_background_job(pid_t pid, const char cmd[], uintptr_t err, uintptr_t data,
 	new->input = NULL;
 	new->output = NULL;
 
+	new->exit_cb = NULL;
+	new->exit_cb_arg = NULL;
+
 #ifndef _WIN32
 	new->err_stream = (int)err;
 #else
@@ -1325,19 +1373,19 @@ add_background_job(pid_t pid, const char cmd[], uintptr_t err, uintptr_t data,
 	if(new->err_stream != NO_JOB_ID)
 	{
 		++new->use_count;
-		pthread_mutex_lock(&new_err_jobs_lock);
+
+		if(pthread_mutex_lock(&new_err_jobs_lock) != 0)
+		{
+			goto free_bg_op_lock;
+		}
 		new->err_next = new_err_jobs;
 		new_err_jobs = new;
-		pthread_mutex_unlock(&new_err_jobs_lock);
-		pthread_cond_signal(&new_err_jobs_cond);
+		(void)pthread_mutex_unlock(&new_err_jobs_lock);
+		(void)pthread_cond_signal(&new_err_jobs_cond);
 	}
 
 	new->with_bg_op = with_bg_op;
 	new->on_job_bar = 0;
-	if(new->with_bg_op)
-	{
-		pthread_spin_init(&new->bg_op_lock, PTHREAD_PROCESS_PRIVATE);
-	}
 	new->bg_op.total = 0;
 	new->bg_op.done = 0;
 	new->bg_op.progress = -1;
@@ -1348,6 +1396,21 @@ add_background_job(pid_t pid, const char cmd[], uintptr_t err, uintptr_t data,
 
 	bg_jobs = new;
 	return new;
+
+free_bg_op_lock:
+	if(with_bg_op)
+	{
+		(void)pthread_spin_destroy(&new->bg_op_lock);
+	}
+free_status_lock:
+	(void)pthread_spin_destroy(&new->status_lock);
+free_errors_lock:
+	(void)pthread_spin_destroy(&new->errors_lock);
+free_cmd:
+	free(new->cmd);
+free_new:
+	free(new);
+	return NULL;
 }
 
 /* pthreads entry point for a new background task.  Performs correct
@@ -1360,35 +1423,22 @@ background_task_bootstrap(void *arg)
 
 	(void)pthread_detach(pthread_self());
 	block_all_thread_signals();
-	set_current_job(task_args->job);
-
-	task_args->func(&task_args->job->bg_op, task_args->args);
 
-	/* Mark task as finished normally. */
-	mark_job_finished(task_args->job, 0);
+	if(pthread_setspecific(current_job, task_args->job) == 0)
+	{
+		task_args->func(&task_args->job->bg_op, task_args->args);
+		mark_job_finished(task_args->job, /*exit_code=*/0);
+	}
+	else
+	{
+		mark_job_finished(task_args->job, /*exit_code=*/1);
+	}
 
 	free(task_args);
 
 	return NULL;
 }
 
-/* Stores pointer to the job in a thread-local storage. */
-static void
-set_current_job(bg_job_t *job)
-{
-	static pthread_once_t once = PTHREAD_ONCE_INIT;
-	pthread_once(&once, &make_current_job_key);
-
-	(void)pthread_setspecific(current_job, job);
-}
-
-/* current_job initializer for pthread_once(). */
-static void
-make_current_job_key(void)
-{
-	(void)pthread_key_create(&current_job, NULL);
-}
-
 int
 bg_has_active_jobs(int important_only)
 {
@@ -1406,6 +1456,13 @@ bg_has_active_jobs(int important_only)
 	return running;
 }
 
+void
+bg_job_set_exit_cb(bg_job_t *job, bg_job_exit_func cb, void *arg)
+{
+	job->exit_cb = cb;
+	job->exit_cb_arg = arg;
+}
+
 int
 bg_job_cancel(bg_job_t *job)
 {
@@ -1478,19 +1535,25 @@ bg_job_terminate(bg_job_t *job)
 int
 bg_job_is_running(bg_job_t *job)
 {
-	pthread_spin_lock(&job->status_lock);
+	if(pthread_spin_lock(&job->status_lock) != 0)
+	{
+		return 1;
+	}
 	int running = job->running;
-	pthread_spin_unlock(&job->status_lock);
+	(void)pthread_spin_unlock(&job->status_lock);
 	return (running && update_job_status(job));
 }
 
 int
 bg_job_was_killed(bg_job_t *job)
 {
-	pthread_spin_lock(&job->status_lock);
+	if(pthread_spin_lock(&job->status_lock) != 0)
+	{
+		return 0;
+	}
 	int running = job->running;
 	int exit_code = job->exit_code;
-	pthread_spin_unlock(&job->status_lock);
+	(void)pthread_spin_unlock(&job->status_lock);
 	return (!running && exit_code >= 0);
 }
 
@@ -1520,7 +1583,7 @@ bg_job_wait(bg_job_t *job)
 	}
 
 #ifndef _WIN32
-	int status = get_proc_exit_status(job->pid);
+	int status = get_proc_exit_status(job->pid, &no_cancellation);
 	if(status == -1)
 	{
 		return 1;
@@ -1567,34 +1630,42 @@ update_job_status(bg_job_t *job)
 static void
 mark_job_finished(bg_job_t *job, int exit_code)
 {
-	pthread_spin_lock(&job->status_lock);
-	job->running = 0;
-	job->exit_code = exit_code;
-	pthread_spin_unlock(&job->status_lock);
+	if(pthread_spin_lock(&job->status_lock) == 0)
+	{
+		job->running = 0;
+		job->exit_code = exit_code;
+		(void)pthread_spin_unlock(&job->status_lock);
+	}
 }
 
 void
 bg_job_incref(bg_job_t *job)
 {
-	pthread_spin_lock(&job->status_lock);
-	++job->use_count;
-	pthread_spin_unlock(&job->status_lock);
+	if(pthread_spin_lock(&job->status_lock) == 0)
+	{
+		++job->use_count;
+		(void)pthread_spin_unlock(&job->status_lock);
+	}
 }
 
 void
 bg_job_decref(bg_job_t *job)
 {
-	pthread_spin_lock(&job->status_lock);
-	--job->use_count;
-	pthread_spin_unlock(&job->status_lock);
+	if(pthread_spin_lock(&job->status_lock) == 0)
+	{
+		--job->use_count;
+		assert(job->use_count >= 0 && "Excessive bg_job_decref() call!");
+		(void)pthread_spin_unlock(&job->status_lock);
+	}
 }
 
-void
+int
 bg_op_lock(bg_op_t *bg_op)
 {
 	bg_job_t *const job = STRUCT_FROM_FIELD(bg_job_t, bg_op, bg_op);
 	assert(job->with_bg_op && "This function requires bg_op data.");
-	pthread_spin_lock(&job->bg_op_lock);
+
+	return (pthread_spin_lock(&job->bg_op_lock) == 0);
 }
 
 void
@@ -1602,7 +1673,10 @@ bg_op_unlock(bg_op_t *bg_op)
 {
 	bg_job_t *const job = STRUCT_FROM_FIELD(bg_job_t, bg_op, bg_op);
 	assert(job->with_bg_op && "This function requires bg_op data.");
-	pthread_spin_unlock(&job->bg_op_lock);
+
+	int error = pthread_spin_unlock(&job->bg_op_lock);
+	assert(error == 0 && "Unlock failure in bg_op_lock()");
+	(void)error;
 }
 
 void
@@ -1614,11 +1688,13 @@ bg_op_changed(bg_op_t *bg_op)
 void
 bg_op_set_descr(bg_op_t *bg_op, const char descr[])
 {
-	bg_op_lock(bg_op);
-	replace_string(&bg_op->descr, descr);
-	bg_op_unlock(bg_op);
+	if(bg_op_lock(bg_op))
+	{
+		replace_string(&bg_op->descr, descr);
+		bg_op_unlock(bg_op);
 
-	bg_op_changed(bg_op);
+		bg_op_changed(bg_op);
+	}
 }
 
 /* Convenience method to cancel background job.  Returns previous version of the
@@ -1626,26 +1702,27 @@ bg_op_set_descr(bg_op_t *bg_op, const char descr[])
 static int
 bg_op_cancel(bg_op_t *bg_op)
 {
-	int was_cancelled;
-
-	bg_op_lock(bg_op);
-	was_cancelled = bg_op->cancelled;
-	bg_op->cancelled = 1;
-	bg_op_unlock(bg_op);
+	int was_cancelled = 0;
+	if(bg_op_lock(bg_op))
+	{
+		was_cancelled = bg_op->cancelled;
+		bg_op->cancelled = 1;
+		bg_op_unlock(bg_op);
 
-	bg_op_changed(bg_op);
+		bg_op_changed(bg_op);
+	}
 	return was_cancelled;
 }
 
 int
 bg_op_cancelled(bg_op_t *bg_op)
 {
-	int cancelled;
-
-	bg_op_lock(bg_op);
-	cancelled = bg_op->cancelled;
-	bg_op_unlock(bg_op);
-
+	int cancelled = 0;
+	if(bg_op_lock(bg_op))
+	{
+		cancelled = bg_op->cancelled;
+		bg_op_unlock(bg_op);
+	}
 	return cancelled;
 }
 
diff --git a/src/background.h b/src/background.h
index d114dd9..13853f7 100644
--- a/src/background.h
+++ b/src/background.h
@@ -35,6 +35,11 @@
  * undefined total number of countable operations. */
 #define BG_UNDEFINED_TOTAL (-1)
 
+struct bg_job_t;
+
+/* Type of a function to invoke when the job is done. */
+typedef void (*bg_job_exit_func)(struct bg_job_t *job, void *data);
+
 /* Type of background job. */
 typedef enum
 {
@@ -54,7 +59,8 @@ typedef enum
 	BJF_SUPPLY_INPUT    = 1 << 2, /* Open a pipe for standard input stream. */
 	BJF_CAPTURE_OUT     = 1 << 3, /* Capture output stream(s). */
 	BJF_MERGE_STREAMS   = 1 << 4, /* Merge error stream into output stream. */
-	BJF_KEEP_SESSION    = 1 << 5, /* Do not detach from terminal session. */
+	BJF_KEEP_IN_FG      = 1 << 5, /* Do not detach from terminal session or
+	                                 process group. */
 }
 BgJobFlags;
 
@@ -97,6 +103,9 @@ typedef struct bg_job_t
 	FILE *input;  /* File stream of standard input or NULL. */
 	FILE *output; /* File stream of standard output or NULL. */
 
+	bg_job_exit_func exit_cb; /* Function to invoke when the job exits. */
+	void *exit_cb_arg;        /* Argument to pass to that function. */
+
 	int with_bg_op;                /* Whether bg_op* fields are active. */
 	int on_job_bar;                /* Whether this task was put on a job bar. */
 	pthread_spinlock_t bg_op_lock; /* Lock for accessing bg_op field. */
@@ -126,8 +135,8 @@ typedef void (*bg_task_func)(bg_op_t *bg_op, void *arg);
 /* List of background jobs. */
 extern bg_job_t *bg_jobs;
 
-/* Prepare background unit for the work. */
-void bg_init(void);
+/* Prepare background unit for the work.  Returns zero on success. */
+int bg_init(void);
 
 /* Creates background job running external command.  Returns zero on success,
  * otherwise non-zero is returned.  If *input is not NULL, it's set to input
@@ -151,10 +160,10 @@ int bg_and_wait_for_errors(char cmd[],
 		const struct cancellation_t *cancellation);
 
 /* Runs command in a background and redirects its stdout and stderr streams to
- * file streams which are set.  Input is redirected only if in parameter isn't
- * NULL.  Don't pass pipe for input, it can cause deadlock.  Returns id of
- * background process ((pid_t)0 for non-*nix like systems) or (pid_t)-1 on
- * error. */
+ * file streams which are set.  Input and output are redirected only if the
+ * corresponding parameter isn't NULL.  Don't pass pipe for input, it can cause
+ * deadlock.  Returns id of background process ((pid_t)0 for non-*nix like
+ * systems) or (pid_t)-1 on error. */
 pid_t bg_run_and_capture(char cmd[], int user_sh, FILE *in, FILE **out,
 		FILE **err);
 
@@ -173,6 +182,9 @@ int bg_execute(const char descr[], const char op_descr[], int total,
  * applications whose state is tracked are always ignored by this function. */
 int bg_has_active_jobs(int important_only);
 
+/* Sets exit callback for the job. */
+void bg_job_set_exit_cb(bg_job_t *job, bg_job_exit_func cb, void *arg);
+
 /* Cancels the job.  Returns non-zero if job wasn't cancelled before, but is
  * after this call, otherwise zero is returned. */
 int bg_job_cancel(bg_job_t *job);
@@ -206,8 +218,8 @@ void bg_job_decref(bg_job_t *job);
 
 /* Temporary locks bg_op_t structure to ensure that it's not modified by
  * anyone during reading/updating its fields.  The structure must be part of
- * bg_job_t. */
-void bg_op_lock(bg_op_t *bg_op);
+ * bg_job_t.  Returns non-zero on success. */
+int bg_op_lock(bg_op_t *bg_op);
 
 /* Unlocks bg_op_t structure.  The structure must be part of bg_job_t. */
 void bg_op_unlock(bg_op_t *bg_op);
diff --git a/src/builtin_functions.c b/src/builtin_functions.c
index ecbf707..fc0be05 100644
--- a/src/builtin_functions.c
+++ b/src/builtin_functions.c
@@ -23,7 +23,7 @@
 #include <assert.h> /* assert() */
 #include <stddef.h> /* NULL size_t */
 #include <stdlib.h> /* free() */
-#include <string.h> /* strcmp() strdup() strpbrk() */
+#include <string.h> /* strcmp() strdup() */
 
 #include "compat/fs_limits.h"
 #include "compat/os.h"
@@ -31,6 +31,7 @@
 #include "engine/text_buffer.h"
 #include "engine/var.h"
 #include "lua/vlua.h"
+#include "modes/cmdline.h"
 #include "ui/cancellation.h"
 #include "ui/tabs.h"
 #include "ui/ui.h"
@@ -44,6 +45,7 @@
 #include "utils/test_helpers.h"
 #include "utils/trie.h"
 #include "utils/utils.h"
+#include "event_loop.h"
 #include "filelist.h"
 #include "macros.h"
 #include "types.h"
@@ -56,17 +58,29 @@ typedef struct
 }
 extcache_t;
 
+/* Data passed to prompt callback in input(). */
+typedef struct
+{
+	int quit;       /* Exit flag for event loop. */
+	char *response; /* Result of the prompt. */
+}
+input_cb_data_t;
+
 static var_t chooseopt_builtin(const call_info_t *call_info);
 static var_t executable_builtin(const call_info_t *call_info);
 static var_t expand_builtin(const call_info_t *call_info);
 static var_t extcached_builtin(const call_info_t *call_info);
 TSTATIC void set_extcached_monitor_type(FileMonType type);
+static var_t filereadable_builtin(const call_info_t *call_info);
 static var_t filetype_builtin(const call_info_t *call_info);
 static int get_fnum(var_t fnum);
 static const char * type_of_link_target(const dir_entry_t *entry);
 static var_t fnameescape_builtin(const call_info_t *call_info);
 static var_t getpanetype_builtin(const call_info_t *call_info);
 static var_t has_builtin(const call_info_t *call_info);
+static var_t input_builtin(const call_info_t *call_info);
+static complete_cmd_func pick_completer(const char type[], int *success);
+static void input_builtin_cb(const char response[], void *arg);
 static var_t layoutis_builtin(const call_info_t *call_info);
 static var_t paneisat_builtin(const call_info_t *call_info);
 static var_t system_builtin(const call_info_t *call_info);
@@ -76,20 +90,22 @@ static var_t execute_cmd(var_t cmd_arg, int interactive, int preserve_stdin);
 
 /* Function descriptions. */
 static const function_t functions[] = {
-	/* Name          Description                    Args   Handler  */
-	{ "chooseopt",   "query choose options",       {1,1}, &chooseopt_builtin },
-	{ "executable",  "check for executable file",  {1,1}, &executable_builtin },
-	{ "expand",      "expand macros in a string",  {1,1}, &expand_builtin },
-	{ "extcached",   "caches result of a command", {3,3}, &extcached_builtin },
-	{ "filetype",    "retrieve type of a file",    {1,2}, &filetype_builtin },
-	{ "fnameescape", "escapes string for a :cmd",  {1,1}, &fnameescape_builtin },
-	{ "getpanetype", "retrieve type of file list", {0,0}, &getpanetype_builtin},
-	{ "has",         "check for specific ability", {1,1}, &has_builtin },
-	{ "layoutis",    "query current layout",       {1,1}, &layoutis_builtin },
-	{ "paneisat",    "query pane location",        {1,1}, &paneisat_builtin },
-	{ "system",      "execute external command",   {1,1}, &system_builtin },
-	{ "tabpagenr",   "number of current/last tab", {0,1}, &tabpagenr_builtin },
-	{ "term",        "run interactive command",    {1,1}, &term_builtin },
+	/* Name           Description                   Args   Handler  */
+	{ "chooseopt",    "query choose options",      {1,1}, &chooseopt_builtin },
+	{ "executable",   "check for executable file", {1,1}, &executable_builtin },
+	{ "expand",       "expand macros in a string", {1,1}, &expand_builtin },
+	{ "extcached",    "caches result of a command",{3,3}, &extcached_builtin },
+	{ "filereadable", "checks for a readable file",{1,1}, &filereadable_builtin },
+	{ "filetype",     "retrieve type of a file",   {1,2}, &filetype_builtin },
+	{ "fnameescape",  "escapes string for a :cmd", {1,1}, &fnameescape_builtin },
+	{ "getpanetype",  "retrieve type of file list",{0,0}, &getpanetype_builtin},
+	{ "has",          "check for specific ability",{1,1}, &has_builtin },
+	{ "input",        "prompt user for input",     {1,3}, &input_builtin },
+	{ "layoutis",     "query current layout",      {1,1}, &layoutis_builtin },
+	{ "paneisat",     "query pane location",       {1,1}, &paneisat_builtin },
+	{ "system",       "execute external command",  {1,1}, &system_builtin },
+	{ "tabpagenr",    "number of current/last tab",{0,1}, &tabpagenr_builtin },
+	{ "term",         "run interactive command",   {1,1}, &term_builtin },
 };
 
 /* Kind of monitor used by the extcached(). */
@@ -148,7 +164,7 @@ executable_builtin(const call_info_t *call_info)
 
 	str_val = var_to_str(call_info->argv[0]);
 
-	if(strpbrk(str_val, PATH_SEPARATORS) != NULL)
+	if(contains_slash(str_val))
 	{
 		exists = executable_exists(str_val);
 	}
@@ -279,6 +295,23 @@ set_extcached_monitor_type(FileMonType type)
 	extcached_mon_type = type;
 }
 
+/* Checks whether path is a non-directory entry and its permissions allow
+ * reading.  Returns boolean value describing result of the check. */
+static var_t
+filereadable_builtin(const call_info_t *call_info)
+{
+	int is_readable = 0;
+
+	char *path = var_to_str(call_info->argv[0]);
+	if(path != NULL)
+	{
+		is_readable = (os_access(path, R_OK) == 0 && !is_dir(path));
+		free(path);
+	}
+
+	return var_from_bool(is_readable);
+}
+
 /* Gets string representation of file type.  Returns the string. */
 static var_t
 filetype_builtin(const call_info_t *call_info)
@@ -351,7 +384,7 @@ fnameescape_builtin(const call_info_t *call_info)
 	var_t result;
 
 	char *const str_val = var_to_str(call_info->argv[0]);
-	char *const escaped = shell_like_escape(str_val, 1);
+	char *const escaped = posix_like_escape(str_val, /*type=*/1);
 	free(str_val);
 
 	result = var_from_str(escaped);
@@ -368,23 +401,91 @@ getpanetype_builtin(const call_info_t *call_info)
 		return var_from_str("regular");
 	}
 
-	switch(curr_view->custom.type)
+	return var_from_str(cv_describe(curr_view->custom.type));
+}
+
+/* Asks user for input and returns the result as a string. */
+static var_t
+input_builtin(const call_info_t *call_info)
+{
+	char *prompt = var_to_str(call_info->argv[0]);
+	char *initial = (call_info->argc > 1) ? var_to_str(call_info->argv[1])
+	                                      : strdup("");
+	char *completion = (call_info->argc > 2) ? var_to_str(call_info->argv[2])
+	                                         : strdup("");
+	if(prompt == NULL || initial == NULL || completion == NULL)
+	{
+		goto fail;
+	}
+
+	input_cb_data_t cb_data = { .quit = 0, .response = NULL };
+
+	int correct_completer;
+	complete_cmd_func complete = pick_completer(completion, &correct_completer);
+	if(!correct_completer)
 	{
-		case CV_REGULAR:
-			return var_from_str("custom");
+		vle_tb_append_linef(vle_err, "Invalid completion type: %s", completion);
+		goto fail;
+	}
+
+	modcline_prompt(prompt, initial, &input_builtin_cb, &cb_data, complete,
+			/*allow_ee=*/1);
+
+	free(prompt);
+	free(initial);
+	free(completion);
+
+	event_loop(&cb_data.quit, /*manage_marking=*/0);
+
+	/* Not returning var_error() on cancellation to allow handling of it by the
+	 * user. */
+	var_t result = var_from_str(cb_data.response == NULL ? "" : cb_data.response);
+
+	free(cb_data.response);
+
+	return result;
+
+fail:
+	free(prompt);
+	free(initial);
+	free(completion);
+	return var_error();
+}
 
-		case CV_VERY:
-			return var_from_str("very-custom");
+/* Callback invoked after prompt has finished. */
+static void
+input_builtin_cb(const char response[], void *arg)
+{
+	input_cb_data_t *data = arg;
 
-		case CV_CUSTOM_TREE:
-		case CV_TREE:
-			return var_from_str("tree");
+	update_string(&data->response, response);
+	data->quit = 1;
+}
 
-		case CV_DIFF:
-		case CV_COMPARE:
-			return var_from_str("compare");
+/* Maps completer name onto a function that implements it.  Always sets *success
+ * to indicate whether parsing was successful (this tells apart empty string
+ * from an invalid one).  Returns function pointer or NULL. */
+static complete_cmd_func
+pick_completer(const char type[], int *success)
+{
+	*success = 1;
+
+	if(strcmp(type, "dir") == 0)
+	{
+		return &modcline_complete_dirs;
 	}
-	return var_from_str("UNKNOWN");
+	else if(strcmp(type, "file") == 0)
+	{
+		return &modcline_complete_files;
+	}
+	else if(strcmp(type, "") == 0)
+	{
+		/* No completion. */
+		return NULL;
+	}
+
+	*success = 0;
+	return NULL;
 }
 
 /* Checks current layout configuration.  Returns boolean value that reflects
diff --git a/src/cfg/config.c b/src/cfg/config.c
index b00b75d..0fee780 100644
--- a/src/cfg/config.c
+++ b/src/cfg/config.c
@@ -154,12 +154,15 @@ cfg_init(void)
 	cfg.tab_stop = 8;
 	cfg.ruler_format = strdup("%l/%S ");
 	cfg.status_line = strdup("");
+	cfg.tab_line = strdup("");
 
 	cfg.lines = INT_MIN;
 	cfg.columns = INT_MIN;
 
 	cfg.dot_dirs = DD_NONROOT_PARENT | DD_TREE_LEAFS_PARENT;
 
+	cfg.mouse = 0;
+
 	cfg.filter_inverted_by_default = 1;
 
 	cfg.apropos_prg = strdup("apropos %a");
@@ -170,6 +173,8 @@ cfg_init(void)
 	cfg.delete_prg = strdup("");
 	cfg.media_prg = format_str("%s/" SAMPLE_MEDIAPRG, get_installed_data_dir());
 
+	cfg.nav_open_files = 0;
+
 	cfg.tail_tab_line_paths = 0;
 	cfg.trunc_normal_sb_msgs = 0;
 	cfg.shorten_title_paths = 1;
@@ -733,11 +738,18 @@ cfg_load(void)
 	 * views. */
 	curr_stats.global_local_settings = 1;
 
-	if(!vifm_testing())
+	/* Try to load global configuration(s). */
+	int i = 0;
+	while(!vifm_testing())
 	{
-		/* Try to load global configuration. */
+		const char *conf_dir = get_sys_conf_dir(i++);
+		if(conf_dir == NULL)
+		{
+			break;
+		}
+
 		char rc_path[PATH_MAX + 1];
-		snprintf(rc_path, sizeof(rc_path), "%s/%s", get_sys_conf_dir(), VIFMRC);
+		build_path(rc_path, sizeof(rc_path), conf_dir, VIFMRC);
 		(void)cfg_source_file(rc_path);
 	}
 
@@ -787,7 +799,7 @@ source_file_internal(strlist_t lines, const char filename[])
 		return 0;
 	}
 
-	commands_scope_start();
+	cmds_scope_start();
 
 	int encoutered_errors = 0;
 
@@ -819,7 +831,7 @@ source_file_internal(strlist_t lines, const char filename[])
 
 		ui_sb_clear();
 
-		if(exec_commands(line, curr_view, CIT_COMMAND) < 0)
+		if(cmds_dispatch(line, curr_view, CIT_COMMAND) < 0)
 		{
 			show_sourcing_error(filename, line_num);
 			encoutered_errors = 1;
@@ -841,7 +853,7 @@ source_file_internal(strlist_t lines, const char filename[])
 
 	ui_sb_clear();
 
-	if(commands_scope_finish() != 0)
+	if(cmds_scope_finish() != 0)
 	{
 		show_sourcing_error(filename, line_num);
 		encoutered_errors = 1;
diff --git a/src/cfg/config.h b/src/cfg/config.h
index 3ebe34a..3634143 100644
--- a/src/cfg/config.h
+++ b/src/cfg/config.h
@@ -46,6 +46,19 @@ typedef enum
 }
 DotDirs;
 
+/* When to handle mouse events. */
+typedef enum
+{
+	M_ALL_MODES    = 1 << 0, /* In all supported modes. */
+	M_NORMAL_MODE  = 1 << 1, /* In normal mode. */
+	M_VISUAL_MODE  = 1 << 2, /* In visual mode. */
+	M_CMDLINE_MODE = 1 << 3, /* In command-line mode (including navmode). */
+	M_MENU_MODE    = 1 << 4, /* In menu mode. */
+	M_VIEW_MODE    = 1 << 5, /* In view mode. */
+	NUM_M_OPTS     =      6  /* Number of options for compile-time checks. */
+}
+Mouse;
+
 /* Tweaks of case sensitivity. */
 typedef enum
 {
@@ -233,11 +246,14 @@ typedef struct config_t
 	int use_system_calls; /* Prefer performing operations with system calls. */
 	int tab_stop;
 	char *ruler_format;
-	char *status_line;
+	char *status_line; /* Format string for status line. */
+	char *tab_line;    /* Nothing or lua handler for status line. */
 	int lines; /* Terminal height in lines. */
 	int columns; /* Terminal width in characters. */
 	/* Controls displaying of dot directories.  Combination of DotDirs flags. */
 	int dot_dirs;
+	/* Controls mouse support.  Combination of Mouse flags. */
+	int mouse;
 	/* File type specific prefixes and suffixes ('classify'). */
 	char type_decs[FT_COUNT][2][9];
 	/* File name specific prefixes and suffixes ('classify'). */
@@ -254,6 +270,8 @@ typedef struct config_t
 	char *delete_prg;  /* File removal application. */
 	char *media_prg;   /* Helper for managing media devices. */
 
+	int nav_open_files; /* Open files on enter key in navigation. */
+
 	/* Message shortening controlled by 'shortmess'. */
 	int tail_tab_line_paths;   /* Display only last directory in tab line. */
 	int trunc_normal_sb_msgs;  /* Truncate normal status bar msgs if needed. */
diff --git a/src/cfg/info.c b/src/cfg/info.c
index f3f467f..cfbc9cb 100644
--- a/src/cfg/info.c
+++ b/src/cfg/info.c
@@ -1033,7 +1033,7 @@ load_cmds(JSON_Object *root)
 			char *cmdadd_cmd = format_str("command %s %s", name, cmd);
 			if(cmdadd_cmd != NULL)
 			{
-				exec_commands(cmdadd_cmd, curr_view, CIT_COMMAND);
+				cmds_dispatch(cmdadd_cmd, curr_view, CIT_COMMAND);
 				free(cmdadd_cmd);
 			}
 		}
@@ -2257,6 +2257,8 @@ store_global_options(JSON_Object *root)
 	append_dstr(options, format_str("rulerformat=%s",
 				escape_spaces(cfg.ruler_format)));
 	append_dstr(options, format_str("%srunexec", cfg.auto_execute ? "" : "no"));
+	append_dstr(options, format_str("navoptions=%s",
+				escape_spaces(vle_opts_get("navoptions", OPT_GLOBAL))));
 	append_dstr(options, format_str("previewoptions=%s",
 				escape_spaces(vle_opts_get("previewoptions", OPT_GLOBAL))));
 	append_dstr(options, format_str("%sscrollbind", cfg.scroll_bind ? "" : "no"));
@@ -2279,6 +2281,8 @@ store_global_options(JSON_Object *root)
 				cfg.sort_numbers ? "" : "no"));
 	append_dstr(options, format_str("statusline=%s",
 				escape_spaces(cfg.status_line)));
+	append_dstr(options, format_str("tabline=%s",
+				escape_spaces(vle_opts_get("tabline", OPT_GLOBAL))));
 	append_dstr(options, format_str("syncregs=%s",
 			escape_spaces(vle_opts_get("syncregs", OPT_GLOBAL))));
 	append_dstr(options, format_str("tablabel=%s",
@@ -2307,6 +2311,8 @@ store_global_options(JSON_Object *root)
 			escape_spaces(vle_opts_get("confirm", OPT_GLOBAL))));
 	append_dstr(options, format_str("dotdirs=%s",
 			escape_spaces(vle_opts_get("dotdirs", OPT_GLOBAL))));
+	append_dstr(options, format_str("mouse=%s",
+			escape_spaces(vle_opts_get("mouse", OPT_GLOBAL))));
 	append_dstr(options, format_str("caseoptions=%s",
 			escape_spaces(vle_opts_get("caseoptions", OPT_GLOBAL))));
 	append_dstr(options, format_str("suggestoptions=%s",
diff --git a/src/cmd_completion.c b/src/cmd_completion.c
index b0db4cb..d96467a 100644
--- a/src/cmd_completion.c
+++ b/src/cmd_completion.c
@@ -195,7 +195,7 @@ non_path_completion(completion_data_t *data)
 		vle_abbr_complete(args);
 		data->start = args;
 	}
-	else if(command_accepts_expr(id))
+	else if(cmds_has_expr_args(id))
 	{
 		complete_expr(arg, &data->start);
 	}
@@ -519,21 +519,29 @@ static void
 complete_compare(const char str[])
 {
 	static const char *lines[][2] = {
-		{ "byname",     "compare by file name" },
-		{ "bysize",     "compare by file size" },
-		{ "bycontents", "compare by file size and hash" },
+		{ "byname",          "compare by file name" },
+		{ "bysize",          "compare by file size" },
+		{ "bycontents",      "compare by file size and hash" },
 
-		{ "ofboth",     "use files of two views" },
-		{ "ofone",      "use files of two current view only" },
+		{ "ofboth",          "use files of two views" },
+		{ "ofone",           "use files of two current view only" },
 
-		{ "listall",    "list all files" },
-		{ "listunique", "list only unique files" },
-		{ "listdups",   "list only duplicated files" },
+		{ "listall",         "list all files" },
+		{ "listunique",      "list only unique files" },
+		{ "listdups",        "list only duplicated files" },
 
-		{ "groupids",   "group files in two panes by ids" },
-		{ "grouppaths", "group files in two panes by paths" },
+		{ "groupids",        "group files in two panes by ids" },
+		{ "grouppaths",      "group files in two panes by paths" },
 
-		{ "skipempty",  "exclude empty files from comparison" },
+		{ "skipempty",       "exclude empty files from comparison" },
+
+		{ "showidentical",   "toggle identical files viewing into comparison" },
+		{ "showdifferent",   "toggle different files viewing into comparison" },
+		{ "showuniqueleft",  "toggle unique left files viewing into comparison" },
+		{ "showuniqueright", "toggle unique right files viewing into comparison" },
+
+		{ "withicase",       "force ignoring case on comparing names" },
+		{ "withrcase",       "force respecting case on comparing names" },
 	};
 
 	complete_from_string_list(str, lines, ARRAY_LEN(lines), 0);
@@ -1382,7 +1390,7 @@ complete_with_shared(const char *server, const char *file)
 				strcat(buf, "/");
 				if(file_matches(buf, file, len))
 				{
-					vle_compl_put_match(shell_like_escape(buf, 1), "");
+					vle_compl_add_path_match(buf);
 				}
 				p++;
 			}
diff --git a/src/cmd_core.c b/src/cmd_core.c
index 54647ba..60e7e00 100644
--- a/src/cmd_core.c
+++ b/src/cmd_core.c
@@ -101,8 +101,6 @@ static int swap_range(void);
 static int resolve_mark(char mark);
 static char * cmds_expand_macros(const char str[], int for_shell, int *usr1,
 		int *usr2);
-static int setup_extcmd_file(const char path[], const char beginning[],
-		CmdInputType type);
 static void prepare_extcmd_file(FILE *fp, const char beginning[],
 		CmdInputType type);
 static hist_t * history_by_type(CmdInputType type);
@@ -125,7 +123,6 @@ static CmdArgsType get_cmd_args_type(const char cmd[]);
 static char * skip_to_cmd_name(const char cmd[]);
 static int repeat_command(view_t *view, CmdInputType type);
 static int is_at_scope_bottom(const int_stack_t *scope_stack);
-TSTATIC char * eval_arglist(const char args[], const char **stop_ptr);
 
 /* Settings for the cmds unit. */
 static cmds_conf_t cmds_conf = {
@@ -186,9 +183,9 @@ cmds_expand_envvars(const char str[])
 }
 
 void
-get_and_execute_command(const char line[], size_t line_pos, CmdInputType type)
+cmds_run_ext(const char line[], size_t line_pos, CmdInputType type)
 {
-	char *const cmd = get_ext_command(line, line_pos, type);
+	char *const cmd = cmds_get_ext(line, line_pos, type);
 	if(cmd == NULL)
 	{
 		save_extcmd(line, type);
@@ -202,45 +199,31 @@ get_and_execute_command(const char line[], size_t line_pos, CmdInputType type)
 }
 
 char *
-get_ext_command(const char beginning[], size_t line_pos, CmdInputType type)
+cmds_get_ext(const char beginning[], size_t line_pos, CmdInputType type)
 {
 	char cmd_file[PATH_MAX + 1];
-	char *cmd = NULL;
-
-	generate_tmp_file_name("vifm.cmdline", cmd_file, sizeof(cmd_file));
-
-	if(setup_extcmd_file(cmd_file, beginning, type) == 0)
+	FILE *file = make_file_in_tmp("vifm.cmdline", 0600, /*auto_delete=*/0,
+			cmd_file, sizeof(cmd_file));
+	if(file == NULL)
 	{
-		if(vim_view_file(cmd_file, 1, line_pos, 0) == 0)
-		{
-			cmd = get_file_first_line(cmd_file);
-		}
+		show_error_msgf("External Editing", "Failed to create a temporary file: %s",
+				strerror(errno));
+		return NULL;
 	}
-	else
+
+	prepare_extcmd_file(file, beginning, type);
+	fclose(file);
+
+	char *cmd = NULL;
+	if(vim_view_file(cmd_file, 1, line_pos, 0) == 0)
 	{
-		show_error_msgf("Error Creating Temporary File",
-				"Could not create file %s: %s", cmd_file, strerror(errno));
+		cmd = get_file_first_line(cmd_file);
 	}
 
 	unlink(cmd_file);
 	return cmd;
 }
 
-/* Create and fill file for external command prompt.  Returns zero on success,
- * otherwise non-zero is returned and errno contains valid value. */
-static int
-setup_extcmd_file(const char path[], const char beginning[], CmdInputType type)
-{
-	FILE *const fp = os_fopen(path, "wt");
-	if(fp == NULL)
-	{
-		return 1;
-	}
-	prepare_extcmd_file(fp, beginning, type);
-	fclose(fp);
-	return 0;
-}
-
 /* Fills the file with history (more recent goes first). */
 static void
 prepare_extcmd_file(FILE *fp, const char beginning[], CmdInputType type)
@@ -307,16 +290,16 @@ execute_extcmd(const char command[], CmdInputType type)
 {
 	if(type == CIT_COMMAND)
 	{
-		commands_scope_start();
-		curr_stats.save_msg = exec_commands(command, curr_view, type);
-		if(commands_scope_finish() != 0)
+		cmds_scope_start();
+		curr_stats.save_msg = cmds_dispatch(command, curr_view, type);
+		if(cmds_scope_finish() != 0)
 		{
 			curr_stats.save_msg = 1;
 		}
 	}
 	else
 	{
-		curr_stats.save_msg = exec_command(command, curr_view, type);
+		curr_stats.save_msg = cmds_dispatch1(command, curr_view, type);
 	}
 }
 
@@ -335,14 +318,14 @@ save_extcmd(const char command[], CmdInputType type)
 }
 
 int
-is_history_command(const char command[])
+cmds_goes_to_history(const char command[])
 {
 	/* Don't add :!! or :! to history list. */
 	return strcmp(command, "!!") != 0 && strcmp(command, "!") != 0;
 }
 
 int
-command_accepts_expr(int cmd_id)
+cmds_has_expr_args(int cmd_id)
 {
 	return cmd_id == COM_ECHO
 	    || cmd_id == COM_EXE
@@ -352,16 +335,16 @@ command_accepts_expr(int cmd_id)
 }
 
 char *
-commands_escape_for_insertion(const char cmd_line[], int pos, const char str[])
+cmds_insertion_escape(const char cmd_line[], int pos, const char str[])
 {
-	const CmdLineLocation ipt = get_cmdline_location(cmd_line, cmd_line + pos);
+	const CmdLineLocation ipt = cmds_classify_pos(cmd_line, cmd_line + pos);
 	switch(ipt)
 	{
 		case CLL_R_QUOTING:
 			/* XXX: Use of filename escape, while special one might be needed. */
 		case CLL_OUT_OF_ARG:
 		case CLL_NO_QUOTING:
-			return shell_like_escape(str, 0);
+			return posix_like_escape(str, /*type=*/0);
 
 		case CLL_S_QUOTING:
 			return escape_for_squotes(str, 0);
@@ -425,7 +408,7 @@ skip_at_beginning(int id, const char args[])
 }
 
 void
-init_commands(void)
+cmds_init(void)
 {
 	if(cmds_conf.inner != NULL)
 	{
@@ -433,7 +416,7 @@ init_commands(void)
 		return;
 	}
 
-	/* We get here when init_commands() is called the first time. */
+	/* We get here when cmds_init() is called the first time. */
 
 	vle_cmds_init(1, &cmds_conf);
 	vle_cmds_add(cmds_list, cmds_list_size);
@@ -501,23 +484,23 @@ pattern_expand_hook(const char pattern[])
 }
 
 int
-cmds_exec(view_t *view, const char command[], int menu, int keep_sel)
+cmds_exec(const char cmd[], view_t *view, int menu, int keep_sel)
 {
 	int id;
 	int result;
 
-	if(command == NULL)
+	if(cmd == NULL)
 	{
 		flist_sel_stash_if_nonempty(view);
 		return 0;
 	}
 
-	command = skip_to_cmd_name(command);
+	cmd = skip_to_cmd_name(cmd);
 
-	if(command[0] == '"')
+	if(cmd[0] == '"')
 		return 0;
 
-	if(command[0] == '\0' && !menu)
+	if(cmd[0] == '\0' && !menu)
 	{
 		flist_sel_stash_if_nonempty(view);
 		return 0;
@@ -531,7 +514,7 @@ cmds_exec(view_t *view, const char command[], int menu, int keep_sel)
 		cmds_conf.end = view->list_rows - 1;
 	}
 
-	id = vle_cmds_identify(command);
+	id = vle_cmds_identify(cmd);
 
 	if(!cmd_should_be_processed(id))
 	{
@@ -543,21 +526,21 @@ cmds_exec(view_t *view, const char command[], int menu, int keep_sel)
 		char undo_msg[COMMAND_GROUP_INFO_LEN];
 
 		snprintf(undo_msg, sizeof(undo_msg), "in %s: %s",
-				replace_home_part(flist_get_dir(view)), command);
+				replace_home_part(flist_get_dir(view)), cmd);
 
 		un_group_open(undo_msg);
 		un_group_close();
 	}
 
 	keep_view_selection = keep_sel;
-	result = vle_cmds_run(command);
+	result = vle_cmds_run(cmd);
 
 	if(result >= 0)
 		return result;
 
-	if(is_implicit_cd(view, command, result))
+	if(is_implicit_cd(view, cmd, result))
 	{
-		return cd(view, flist_get_dir(view), command);
+		return cd(view, flist_get_dir(view), cmd);
 	}
 
 	switch(result)
@@ -840,7 +823,7 @@ line_pos(const char begin[], const char end[], char sep, int rquoting,
 }
 
 int
-exec_commands(const char cmdline[], view_t *view, CmdInputType type)
+cmds_dispatch(const char cmdline[], view_t *view, CmdInputType type)
 {
 	int save_msg = 0;
 	char **cmds = break_cmdline(cmdline, type == CIT_MENU_COMMAND);
@@ -848,7 +831,7 @@ exec_commands(const char cmdline[], view_t *view, CmdInputType type)
 
 	while(*cmd != NULL)
 	{
-		const int ret = exec_command(*cmd, view, type);
+		const int ret = cmds_dispatch1(*cmd, view, type);
 		if(ret != 0)
 		{
 			save_msg = (ret < 0) ? -1 : 1;
@@ -976,7 +959,7 @@ finish:
 static int
 is_out_of_arg(const char cmd[], const char pos[])
 {
-	const CmdLineLocation location = get_cmdline_location(cmd, pos);
+	const CmdLineLocation location = cmds_classify_pos(cmd, pos);
 
 	if(location == CLL_NO_QUOTING)
 	{
@@ -996,7 +979,7 @@ is_out_of_arg(const char cmd[], const char pos[])
 }
 
 CmdLineLocation
-get_cmdline_location(const char cmd[], const char pos[])
+cmds_classify_pos(const char cmd[], const char *pos)
 {
 	char separator;
 	int regex_quoting;
@@ -1056,6 +1039,8 @@ get_cmd_args_type(const char cmd[])
 	switch(cmd_id)
 	{
 		case COMMAND_CMD_ID:
+		case COM_AMAP:
+		case COM_ANOREMAP:
 		case COM_AUTOCMD:
 		case COM_EXECUTE:
 		case COM_CABBR:
@@ -1085,12 +1070,12 @@ get_cmd_args_type(const char cmd[])
 			return CAT_UNTIL_THE_END;
 
 		default:
-			return command_accepts_expr(cmd_id) ? CAT_EXPR : CAT_REGULAR;
+			return cmds_has_expr_args(cmd_id) ? CAT_EXPR : CAT_REGULAR;
 	}
 }
 
 const char *
-find_last_command(const char cmds[])
+cmds_find_last(const char cmds[])
 {
 	const char *p, *q;
 
@@ -1148,10 +1133,11 @@ skip_to_cmd_name(const char cmd[])
 }
 
 int
-exec_command(const char cmd[], view_t *view, CmdInputType type)
+cmds_dispatch1(const char cmd[], view_t *view, CmdInputType type)
 {
 	int menu;
 	int backward;
+	int found;
 
 	if(cmd == NULL)
 	{
@@ -1164,11 +1150,11 @@ exec_command(const char cmd[], view_t *view, CmdInputType type)
 	{
 		case CIT_BSEARCH_PATTERN: backward = 1; /* Fall through. */
 		case CIT_FSEARCH_PATTERN:
-			return modnorm_find(view, cmd, backward, 1);
+			return modnorm_find(view, cmd, backward, /*print_msg=*/1, &found);
 
 		case CIT_VBSEARCH_PATTERN: backward = 1; /* Fall through. */
 		case CIT_VFSEARCH_PATTERN:
-			return modvis_find(view, cmd, backward, 1);
+			return modvis_find(view, cmd, backward, /*print_msg=*/1, &found);
 
 		case CIT_VWBSEARCH_PATTERN: backward = 1; /* Fall through. */
 		case CIT_VWFSEARCH_PATTERN:
@@ -1176,7 +1162,7 @@ exec_command(const char cmd[], view_t *view, CmdInputType type)
 
 		case CIT_MENU_COMMAND: menu = 1; /* Fall through. */
 		case CIT_COMMAND:
-			return cmds_exec(view, cmd, menu, /*keep_sel=*/0);
+			return cmds_exec(cmd, view, menu, /*keep_sel=*/0);
 
 		case CIT_FILTER_PATTERN:
 			if(view->custom.type != CV_DIFF)
@@ -1202,22 +1188,25 @@ static int
 repeat_command(view_t *view, CmdInputType type)
 {
 	int backward = 0;
+	int found;
 	switch(type)
 	{
 		case CIT_BSEARCH_PATTERN: backward = 1; /* Fall through. */
 		case CIT_FSEARCH_PATTERN:
-			return modnorm_find(view, hists_search_last(), backward, 1);
+			return modnorm_find(view, hists_search_last(), backward, /*print_msg=*/1,
+					&found);
 
 		case CIT_VBSEARCH_PATTERN: backward = 1; /* Fall through. */
 		case CIT_VFSEARCH_PATTERN:
-			return modvis_find(view, hists_search_last(), backward, 1);
+			return modvis_find(view, hists_search_last(), backward, /*print_msg=*/1,
+					&found);
 
 		case CIT_VWBSEARCH_PATTERN: backward = 1; /* Fall through. */
 		case CIT_VWFSEARCH_PATTERN:
 			return modview_find(NULL, backward);
 
 		case CIT_COMMAND:
-			return cmds_exec(view, NULL, /*menu=*/0, /*keep_sel=*/0);
+			return cmds_exec(/*cmd=*/NULL, view, /*menu=*/0, /*keep_sel=*/0);
 
 		case CIT_FILTER_PATTERN:
 			local_filter_apply(view, "");
@@ -1230,13 +1219,13 @@ repeat_command(view_t *view, CmdInputType type)
 }
 
 void
-commands_scope_start(void)
+cmds_scope_start(void)
 {
 	(void)int_stack_push(&if_levels, IF_SCOPE_GUARD);
 }
 
 void
-commands_scope_escape(void)
+cmds_scope_escape(void)
 {
 	while(!is_at_scope_bottom(&if_levels))
 	{
@@ -1245,7 +1234,7 @@ commands_scope_escape(void)
 }
 
 int
-commands_scope_finish(void)
+cmds_scope_finish(void)
 {
 	if(!is_at_scope_bottom(&if_levels))
 	{
@@ -1338,7 +1327,7 @@ is_at_scope_bottom(const int_stack_t *scope_stack)
 }
 
 char *
-eval_arglist(const char args[], const char **stop_ptr)
+cmds_eval_args(const char args[], const char **stop_ptr)
 {
 	size_t len = 0;
 	char *eval_result = NULL;
@@ -1350,30 +1339,28 @@ eval_arglist(const char args[], const char **stop_ptr)
 		char *free_this = NULL;
 		const char *tmp_result = NULL;
 
-		var_t result = var_false();
-		const ParsingErrors parsing_error = parse(args, 1, &result);
-		if(parsing_error == PE_INVALID_EXPRESSION && is_prev_token_whitespace())
+		parsing_result_t result = vle_parser_eval(args, /*interactive=*/1);
+		if(result.error == PE_INVALID_EXPRESSION && result.ends_with_whitespace)
 		{
-			result = get_parsing_result();
-			if(result.type != VTYPE_ERROR)
+			if(result.value.type != VTYPE_ERROR)
 			{
-				tmp_result = free_this = var_to_str(result);
-				args = get_last_parsed_char();
+				tmp_result = free_this = var_to_str(result.value);
+				args = result.last_parsed_char;
 			}
 		}
-		else if(parsing_error == PE_NO_ERROR)
+		else if(result.error == PE_NO_ERROR)
 		{
-			tmp_result = free_this = var_to_str(result);
-			args = get_last_position();
+			tmp_result = free_this = var_to_str(result.value);
+			args = result.last_position;
 		}
-		else if(parsing_error != PE_INVALID_EXPRESSION)
+		else if(result.error != PE_INVALID_EXPRESSION)
 		{
-			report_parsing_error(parsing_error);
+			vle_parser_report(&result);
 		}
 
 		if(tmp_result == NULL)
 		{
-			var_free(result);
+			var_free(result.value);
 			break;
 		}
 
@@ -1383,7 +1370,7 @@ eval_arglist(const char args[], const char **stop_ptr)
 		}
 		eval_result = extend_string(eval_result, tmp_result, &len);
 
-		var_free(result);
+		var_free(result.value);
 		free(free_this);
 
 		args = skip_whitespace(args);
diff --git a/src/cmd_core.h b/src/cmd_core.h
index caebf37..e8648a5 100644
--- a/src/cmd_core.h
+++ b/src/cmd_core.h
@@ -56,42 +56,43 @@ CmdLineLocation;
 
 struct view_t;
 
-void init_commands(void);
+/* Initializes use of :commands and its dependencies: bracket notation and
+ * variables. */
+void cmds_init(void);
 
 /* Executes one or more commands separated by a bar.  Returns zero on success if
  * no message should be saved in the status bar, positive value to save message
  * on successful execution and negative value in case of error with error
  * message. */
-int exec_commands(const char cmd[], struct view_t *view, CmdInputType type);
+int cmds_dispatch(const char cmd[], struct view_t *view, CmdInputType type);
 
-/* Executes command of specified kind.  Returns zero on success if no message
- * should be saved in the status bar, positive value to save message on
+/* Executes single command of specified kind.  Returns zero on success if no
+ * message should be saved in the status bar, positive value to save message on
  * successful execution and negative value in case of error with error
  * message. */
-int exec_command(const char cmd[], struct view_t *view, CmdInputType type);
+int cmds_dispatch1(const char cmd[], struct view_t *view, CmdInputType type);
 
 /* Executes a single command-line command.  Returns negative value in case of
  * an error or value from command handler. */
-int cmds_exec(struct view_t *view, const char command[], int menu,
-		int keep_sel);
+int cmds_exec(const char cmd[], struct view_t *view, int menu, int keep_sel);
 
 /* Should precede new command execution scope (e.g. before start of sourced
  * script). */
-void commands_scope_start(void);
+void cmds_scope_start(void);
 
 /* Marks active command execution scope as escaped meaning that there is no need
  * to check for endif. */
-void commands_scope_escape(void);
+void cmds_scope_escape(void);
 
 /* Should terminate command execution scope (e.g. end of sourced script).
  * Performs some of internal checks.  Returns non-zero when there were errors,
  * otherwise zero is returned. */
-int commands_scope_finish(void);
+int cmds_scope_finish(void);
 
 /* Find start of the last command in pipe-separated list of command-line
  * commands.  Accounts for pipe escaping.  Returns pointer to start of the last
  * command. */
-const char * find_last_command(const char cmds[]);
+const char * cmds_find_last(const char cmds[]);
 
 /* Expands all environment variables in the str.  Allocates and returns memory
  * that should be freed by the caller. */
@@ -99,38 +100,35 @@ char * cmds_expand_envvars(const char str[]);
 
 /* Opens the editor with the line at given column, gets entered command and
  * executes it in the way dependent on the type of command. */
-void get_and_execute_command(const char line[], size_t line_pos,
-		CmdInputType type);
+void cmds_run_ext(const char line[], size_t line_pos, CmdInputType type);
 
 /* Opens the editor with the beginning at the line_pos column.  Type is used to
  * provide useful context.  On success returns entered command as a newly
  * allocated string, which should be freed by the caller, otherwise NULL is
  * returned. */
-char * get_ext_command(const char beginning[], size_t line_pos,
-		CmdInputType type);
+char * cmds_get_ext(const char beginning[], size_t line_pos, CmdInputType type);
 
 /* Checks whether command should be stored in command-line history.  Returns
  * non-zero if it should be stored, otherwise zero is returned. */
-int is_history_command(const char command[]);
+int cmds_goes_to_history(const char command[]);
 
 /* Checks whether command accepts exception as its argument(s).  Returns
  * non-zero if so, otherwise zero is returned. */
-int command_accepts_expr(int cmd_id);
+int cmds_has_expr_args(int cmd_id);
 
 /* Analyzes command line at given position and escapes str accordingly.  Returns
  * escaped string or NULL when no escaping is needed. */
-char * commands_escape_for_insertion(const char cmd_line[], int pos,
-		const char str[]);
+char * cmds_insertion_escape(const char cmd_line[], int pos, const char str[]);
 
 /* Analyzes position on the command-line.  pos should point to some position of
  * cmd.  Returns where current position in the command line is. */
-CmdLineLocation get_cmdline_location(const char cmd[], const char pos[]);
+CmdLineLocation cmds_classify_pos(const char cmd[], const char *pos);
 
 /* Evaluates a set of expressions and concatenates results with a space.  args
  * can not be empty string.  Returns pointer to newly allocated string, which
  * should be freed by caller, or NULL on error.  stop_ptr will point to the
  * beginning of invalid expression in case of error. */
-char * eval_arglist(const char args[], const char **stop_ptr);
+char * cmds_eval_args(const char args[], const char **stop_ptr);
 
 /* Requests unit to do not reset selection after command execution.  Expected to
  * be called from command handlers, or it won't have any effect. */
diff --git a/src/cmd_handlers.c b/src/cmd_handlers.c
index 91d6186..43f5ac4 100644
--- a/src/cmd_handlers.c
+++ b/src/cmd_handlers.c
@@ -27,14 +27,14 @@
 
 #include <assert.h> /* assert() */
 #include <ctype.h> /* isdigit() */
-#include <errno.h>
+#include <errno.h> /* errno */
 #include <limits.h> /* INT_MAX */
 #include <signal.h>
 #include <stddef.h> /* NULL size_t */
 #include <stdio.h> /* snprintf() */
 #include <stdlib.h> /* EXIT_SUCCESS atoi() free() realloc() */
 #include <string.h> /* strchr() strcmp() strcspn() strcasecmp() strcpy()
-                       strdup() strlen() strrchr() strspn() */
+                       strdup() strerror() strlen() strrchr() strspn() */
 #include <wctype.h> /* iswspace() */
 #include <wchar.h> /* wcslen() wcsncmp() */
 
@@ -55,6 +55,7 @@
 #include "int/path_env.h"
 #include "int/vim.h"
 #include "lua/vlua.h"
+#include "modes/normal.h"
 #include "modes/dialogs/attr_dialog.h"
 #include "modes/dialogs/change_dialog.h"
 #include "modes/dialogs/msg_dialog.h"
@@ -117,11 +118,14 @@
 static int goto_cmd(const cmd_info_t *cmd_info);
 static int emark_cmd(const cmd_info_t *cmd_info);
 static int alink_cmd(const cmd_info_t *cmd_info);
+static int amap_cmd(const cmd_info_t *cmd_info);
+static int anoremap_cmd(const cmd_info_t *cmd_info);
 static int apropos_cmd(const cmd_info_t *cmd_info);
 static int autocmd_cmd(const cmd_info_t *cmd_info);
 static void aucmd_list_cb(const char event[], const char pattern[], int negated,
 		const char action[], void *arg);
 static void aucmd_action_handler(const char action[], void *arg);
+static int aunmap_cmd(const cmd_info_t *cmd_info);
 static int bmark_cmd(const cmd_info_t *cmd_info);
 static int bmarks_cmd(const cmd_info_t *cmd_info);
 static int bmgo_cmd(const cmd_info_t *cmd_info);
@@ -154,7 +158,7 @@ static int command_cmd(const cmd_info_t *cmd_info);
 static int compare_cmd(const cmd_info_t *cmd_info);
 static int copen_cmd(const cmd_info_t *cmd_info);
 static int parse_compare_properties(const cmd_info_t *cmd_info, CompareType *ct,
-		ListType *lt, int *single_pane, int *group_ids, int *skip_empty);
+		ListType *lt, int *flags);
 static int cunmap_cmd(const cmd_info_t *cmd_info);
 static int delete_cmd(const cmd_info_t *cmd_info);
 static int delmarks_cmd(const cmd_info_t *cmd_info);
@@ -176,7 +180,7 @@ static int elseif_cmd(const cmd_info_t *cmd_info);
 static int empty_cmd(const cmd_info_t *cmd_info);
 static int endif_cmd(const cmd_info_t *cmd_info);
 static int exe_cmd(const cmd_info_t *cmd_info);
-static char * try_eval_arglist(const cmd_info_t *cmd_info);
+static char * try_eval_args(const cmd_info_t *cmd_info);
 static int file_cmd(const cmd_info_t *cmd_info);
 static int filetype_cmd(const cmd_info_t *cmd_info);
 static int filextype_cmd(const cmd_info_t *cmd_info);
@@ -259,6 +263,7 @@ static int qmap_cmd(const cmd_info_t *cmd_info);
 static int qnoremap_cmd(const cmd_info_t *cmd_info);
 static int qunmap_cmd(const cmd_info_t *cmd_info);
 static int redraw_cmd(const cmd_info_t *cmd_info);
+static int regedit_cmd(const cmd_info_t *cmd_info);
 static int registers_cmd(const cmd_info_t *cmd_info);
 static int regular_cmd(const cmd_info_t *cmd_info);
 static int rename_cmd(const cmd_info_t *cmd_info);
@@ -354,10 +359,22 @@ const cmd_add_t cmds_list[] = {
 	  .flags = HAS_EMARK | HAS_RANGE | HAS_QUOTED_ARGS | HAS_COMMENT
 	         | HAS_QMARK_NO_ARGS | HAS_SELECTION_SCOPE,
 	  .handler = &alink_cmd,       .min_args = 0,   .max_args = NOT_DEF, },
+	{ .name = "amap",              .abbr = NULL,    .id = COM_AMAP,
+	  .descr = "map keys in navigation mode",
+	  .flags = HAS_RAW_ARGS,
+	  .handler = &amap_cmd,        .min_args = 0,   .max_args = NOT_DEF, },
+	{ .name = "anoremap",          .abbr = NULL,    .id = COM_ANOREMAP,
+	  .descr = "noremap keys in navigation mode",
+	  .flags = HAS_RAW_ARGS,
+	  .handler = &anoremap_cmd,    .min_args = 0,   .max_args = NOT_DEF, },
 	{ .name = "apropos",           .abbr = NULL,    .id = -1,
 	  .descr = "query apropos results",
 	  .flags = 0,
 	  .handler = &apropos_cmd,     .min_args = 0,   .max_args = NOT_DEF, },
+	{ .name = "aunmap",            .abbr = NULL,    .id = -1,
+	  .descr = "unmap user keys in navigation mode",
+	  .flags = HAS_RAW_ARGS,
+	  .handler = &aunmap_cmd,      .min_args = 1,   .max_args = 1, },
 	{ .name = "autocmd",           .abbr = "au",    .id = COM_AUTOCMD,
 	  .descr = "manage autocommands",
 	  .flags = HAS_EMARK | HAS_QUOTED_ARGS,
@@ -433,7 +450,7 @@ const cmd_add_t cmds_list[] = {
 	  .handler = &command_cmd,     .min_args = 0,   .max_args = NOT_DEF, },
 	{ .name = "compare",           .abbr = NULL,    .id = COM_COMPARE,
 	  .descr = "compare directories in two panes",
-	  .flags = HAS_COMMENT,
+	  .flags = HAS_EMARK | HAS_COMMENT,
 	  .handler = &compare_cmd,     .min_args = 0,   .max_args = NOT_DEF, },
 	{ .name = "copen",             .abbr = "cope",  .id = -1,
 	  .descr = "reopen last displayed navigation menu",
@@ -742,6 +759,10 @@ const cmd_add_t cmds_list[] = {
 	  .descr = "force screen redraw",
 	  .flags = HAS_COMMENT,
 	  .handler = &redraw_cmd,      .min_args = 0,   .max_args = 0, },
+	{ .name = "regedit",           .abbr = "rege",  .id = -1,
+	  .descr = "edit register contents",
+	  .flags = HAS_COMMENT | HAS_RAW_ARGS,
+	  .handler = &regedit_cmd,     .min_args = 0,   .max_args = 1 },
 	{ .name = "registers",         .abbr = "reg",   .id = -1,
 	  .descr = "display registers",
 	  .flags = 0,
@@ -1014,7 +1035,7 @@ emark_cmd(const cmd_info_t *cmd_info)
 				ui_sb_msg("No previous command-line command");
 				return 1;
 			}
-			return exec_commands(last_cmd, curr_view, CIT_COMMAND) != 0;
+			return cmds_dispatch(last_cmd, curr_view, CIT_COMMAND) != 0;
 		}
 		return CMDS_ERR_TOO_FEW_ARGS;
 	}
@@ -1090,6 +1111,20 @@ alink_cmd(const cmd_info_t *cmd_info)
 	return link_cmd(cmd_info, 1);
 }
 
+/* Registers a navigation mapping that allows remapping. */
+static int
+amap_cmd(const cmd_info_t *cmd_info)
+{
+	return do_map(cmd_info, "Navigation", NAV_MODE, /*no_remap=*/0) != 0;
+}
+
+/* Registers a navigation mapping that doesn't allow remapping. */
+static int
+anoremap_cmd(const cmd_info_t *cmd_info)
+{
+	return do_map(cmd_info, "Navigation", NAV_MODE, /*no_remap=*/1) != 0;
+}
+
 static int
 apropos_cmd(const cmd_info_t *cmd_info)
 {
@@ -1102,7 +1137,7 @@ apropos_cmd(const cmd_info_t *cmd_info)
 	else if(last_args == NULL)
 	{
 		ui_sb_err("Nothing to repeat");
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	return show_apropos_menu(curr_view, last_args) != 0;
@@ -1147,7 +1182,7 @@ autocmd_cmd(const cmd_info_t *cmd_info)
 		if(!is_in_string_array_case(events, ARRAY_LEN(events), event))
 		{
 			ui_sb_errf("No such event: %s", event);
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 	}
 
@@ -1173,7 +1208,7 @@ autocmd_cmd(const cmd_info_t *cmd_info)
 	if(vle_aucmd_on_execute(event, patterns, action, &aucmd_action_handler) != 0)
 	{
 		ui_sb_err("Failed to register autocommand");
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	return 0;
@@ -1193,7 +1228,7 @@ aucmd_action_handler(const char action[], void *arg)
 	const int prev_global_local_settings = curr_stats.global_local_settings;
 	curr_stats.global_local_settings = 0;
 
-	(void)exec_commands(action, view, CIT_COMMAND);
+	(void)cmds_dispatch(action, view, CIT_COMMAND);
 
 	curr_stats.global_local_settings = prev_global_local_settings;
 
@@ -1215,6 +1250,13 @@ aucmd_list_cb(const char event[], const char pattern[], int negated,
 	vle_tb_append_linef(msg, fmt, event, negated ? "!" : "", pattern, action);
 }
 
+/* Unregisters a navigation mapping. */
+static int
+aunmap_cmd(const cmd_info_t *cmd_info)
+{
+	return do_unmap(cmd_info->argv[0], NAV_MODE);
+}
+
 /* Marks directory with set of tags. */
 static int
 bmark_cmd(const cmd_info_t *cmd_info)
@@ -1228,7 +1270,7 @@ bmark_cmd(const cmd_info_t *cmd_info)
 	}
 	free(path);
 	free(tags);
-	return err;
+	return (err ? CMDS_ERR_CUSTOM : 0);
 }
 
 /* Lists either all bookmarks or those matching specified tags. */
@@ -1403,7 +1445,6 @@ list_abbrevs(const char prefix[])
 static int
 add_cabbrev(const cmd_info_t *cmd_info, int no_remap)
 {
-	int result;
 	wchar_t *subst;
 	wchar_t *wargs = to_wide(cmd_info->args);
 	wchar_t *rhs = wargs;
@@ -1425,18 +1466,19 @@ add_cabbrev(const cmd_info_t *cmd_info, int no_remap)
 	}
 
 	subst = substitute_specsw(rhs);
-	result = no_remap
-	       ? vle_abbr_add_no_remap(wargs, subst)
-	       : vle_abbr_add(wargs, subst);
+	int err = no_remap
+	        ? vle_abbr_add_no_remap(wargs, subst)
+	        : vle_abbr_add(wargs, subst);
 	free(subst);
 	free(wargs);
 
-	if(result != 0)
+	if(err != 0)
 	{
 		ui_sb_err("Failed to register abbreviation");
+		return CMDS_ERR_CUSTOM;
 	}
 
-	return result;
+	return 0;
 }
 
 /* Changes location of a view or both views.  Handle multiple configurations of
@@ -1584,7 +1626,7 @@ chmod_cmd(const cmd_info_t *cmd_info)
 	{
 		ui_sb_errf("Regexp error: %s", get_regexp_error(err, &re));
 		regfree(&re);
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	for(i = 0; i < cmd_info->argc; i++)
@@ -1599,7 +1641,7 @@ chmod_cmd(const cmd_info_t *cmd_info)
 	if(i < cmd_info->argc)
 	{
 		ui_sb_errf("Invalid argument: %s", cmd_info->argv[i]);
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	flist_set_marking(curr_view, 0);
@@ -1646,12 +1688,12 @@ chown_cmd(const cmd_info_t *cmd_info)
 	if(u && get_uid(user, &uid) != 0)
 	{
 		ui_sb_errf("Invalid user name: \"%s\"", user);
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 	if(g && get_gid(group, &gid) != 0)
 	{
 		ui_sb_errf("Invalid group name: \"%s\"", group);
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	flist_set_marking(curr_view, 0);
@@ -1670,7 +1712,7 @@ clone_cmd(const cmd_info_t *cmd_info)
 		if(cmd_info->argc > 0)
 		{
 			ui_sb_err("No arguments are allowed if you use \"?\"");
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 		return fops_clone(curr_view, NULL, -1, 0, 1) != 0;
 	}
@@ -1712,7 +1754,7 @@ colorscheme_cmd(const cmd_info_t *cmd_info)
 	if((cmd_info->argc == 1 || assoc_form) && !cs_exists(cmd_info->argv[0]))
 	{
 		ui_sb_errf("Cannot find colorscheme %s" , cmd_info->argv[0]);
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	if(assoc_form)
@@ -1765,7 +1807,7 @@ assoc_colorscheme(const char name[], const char path[])
 	{
 		ui_sb_errf("%s isn't a directory", directory);
 		free(directory);
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	cs_assoc_dir(name, directory);
@@ -1853,11 +1895,13 @@ cunabbrev_cmd(const cmd_info_t *cmd_info)
 		return 0;
 	}
 
-	const int result = vle_abbr_remove(wargs);
+	const int err = vle_abbr_remove(wargs);
 	free(wargs);
-	if(result != 0)
+
+	if(err != 0)
 	{
-		ui_sb_err("No such abbreviation");
+		ui_sb_errf("No such abbreviation: %s", cmd_info->args);
+		return CMDS_ERR_CUSTOM;
 	}
 	return 0;
 }
@@ -1910,7 +1954,7 @@ delmarks_cmd(const cmd_info_t *cmd_info)
 		else
 		{
 			ui_sb_err("No arguments are allowed if you use \"!\"");
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 	}
 
@@ -1976,6 +2020,11 @@ delbmarks_cmd(const cmd_info_t *cmd_info)
 	{
 		/* Remove set of bookmarks that include all of the specified tags. */
 		char *const tags = make_tags_list(cmd_info);
+		if(tags == NULL)
+		{
+			return CMDS_ERR_CUSTOM;
+		}
+
 		bmarks_find(tags, &remove_bmark, NULL);
 		free(tags);
 	}
@@ -2028,18 +2077,36 @@ make_bmark_path(const char path[])
 static int
 compare_cmd(const cmd_info_t *cmd_info)
 {
+	int is_toggling = cmd_info->emark;
+	if(is_toggling && !cv_compare(curr_view->custom.type))
+	{
+		ui_sb_err("Toggling requires active compare view");
+		return CMDS_ERR_CUSTOM;
+	}
+
 	CompareType ct = CT_CONTENTS;
 	ListType lt = LT_ALL;
-	int single_pane = 0, group_ids = 0, skip_empty = 0;
-	if(parse_compare_properties(cmd_info, &ct, &lt, &single_pane,
-				&group_ids, &skip_empty) != 0)
+	int flags = (is_toggling ? CF_NONE : CF_GROUP_PATHS);
+	if(parse_compare_properties(cmd_info, &ct, &lt, &flags) != 0)
 	{
 		return CMDS_ERR_CUSTOM;
 	}
 
-	return single_pane
-	     ? (compare_one_pane(curr_view, ct, lt, skip_empty) != 0)
-	     : (compare_two_panes(ct, lt, !group_ids, skip_empty) != 0);
+	if(is_toggling)
+	{
+		struct cv_data_t *cv = &curr_view->custom;
+		return (compare_two_panes(cv->diff_cmp_type, cv->diff_list_type,
+					cv->diff_cmp_flags ^ flags) != 0);
+	}
+
+	if((flags & CF_SHOW) == 0)
+	{
+		flags |= CF_SHOW;
+	}
+
+	return (flags & CF_SINGLE_PANE)
+	     ? (compare_one_pane(curr_view, ct, lt, flags) != 0)
+	     : (compare_two_panes(ct, lt, flags) != 0);
 }
 
 /* Opens menu with contents of the last displayed menu with navigation to files
@@ -2055,27 +2122,59 @@ copen_cmd(const cmd_info_t *cmd_info)
  * error message is displayed on the status bar. */
 static int
 parse_compare_properties(const cmd_info_t *cmd_info, CompareType *ct,
-		ListType *lt, int *single_pane, int *group_ids, int *skip_empty)
+		ListType *lt, int *flags)
 {
 	int i;
 	for(i = 0; i < cmd_info->argc; ++i)
 	{
 		const char *const property = cmd_info->argv[i];
+
+		if(cmd_info->emark && !starts_with_lit(property, "show"))
+		{
+			ui_sb_errf("Unexpected property for toggling: %s", property);
+			return CMDS_ERR_CUSTOM;
+		}
+
 		if     (strcmp(property, "byname") == 0)     *ct = CT_NAME;
 		else if(strcmp(property, "bysize") == 0)     *ct = CT_SIZE;
 		else if(strcmp(property, "bycontents") == 0) *ct = CT_CONTENTS;
+
 		else if(strcmp(property, "listall") == 0)    *lt = LT_ALL;
 		else if(strcmp(property, "listunique") == 0) *lt = LT_UNIQUE;
 		else if(strcmp(property, "listdups") == 0)   *lt = LT_DUPS;
-		else if(strcmp(property, "ofboth") == 0)     *single_pane = 0;
-		else if(strcmp(property, "ofone") == 0)      *single_pane = 1;
-		else if(strcmp(property, "groupids") == 0)   *group_ids = 1;
-		else if(strcmp(property, "grouppaths") == 0) *group_ids = 0;
-		else if(strcmp(property, "skipempty") == 0)  *skip_empty = 1;
+
+		else if(strcmp(property, "ofboth") == 0)     *flags &= ~CF_SINGLE_PANE;
+		else if(strcmp(property, "ofone") == 0)      *flags |= CF_SINGLE_PANE;
+
+		else if(strcmp(property, "groupids") == 0)   *flags &= ~CF_GROUP_PATHS;
+		else if(strcmp(property, "grouppaths") == 0) *flags |= CF_GROUP_PATHS;
+
+		else if(strcmp(property, "skipempty") == 0)  *flags |= CF_SKIP_EMPTY;
+
+		else if(strcmp(property, "showidentical") == 0)
+			*flags |= CF_SHOW_IDENTICAL;
+		else if(strcmp(property, "showdifferent") == 0)
+			*flags |= CF_SHOW_DIFFERENT;
+		else if(strcmp(property, "showuniqueleft") == 0)
+			*flags |= CF_SHOW_UNIQUE_LEFT;
+		else if(strcmp(property, "showuniqueright") == 0)
+			*flags |= CF_SHOW_UNIQUE_RIGHT;
+
+		else if(strcmp(property, "withicase") == 0)
+		{
+			*flags &= ~CF_RESPECT_CASE;
+			*flags |= CF_IGNORE_CASE;
+		}
+		else if(strcmp(property, "withrcase") == 0)
+		{
+			*flags &= ~CF_IGNORE_CASE;
+			*flags |= CF_RESPECT_CASE;
+		}
+
 		else
 		{
 			ui_sb_errf("Unknown comparison property: %s", property);
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 	}
 
@@ -2090,14 +2189,14 @@ delsession_cmd(const cmd_info_t *cmd_info)
 
 	if(!sessions_exists(session_name))
 	{
-		ui_sb_msgf("No stored sessions with such name: %s", session_name);
-		return 1;
+		ui_sb_errf("No stored sessions with such name: %s", session_name);
+		return CMDS_ERR_CUSTOM;
 	}
 
 	if(sessions_remove(session_name) != 0)
 	{
-		ui_sb_msgf("Failed to delete a session: %s", session_name);
-		return 1;
+		ui_sb_errf("Failed to delete a session: %s", session_name);
+		return CMDS_ERR_CUSTOM;
 	}
 
 	return 0;
@@ -2160,7 +2259,12 @@ dunmap_cmd(const cmd_info_t *cmd_info)
 static int
 echo_cmd(const cmd_info_t *cmd_info)
 {
-	char *const eval_result = try_eval_arglist(cmd_info);
+	char *const eval_result = try_eval_args(cmd_info);
+	if(eval_result == NULL)
+	{
+		return CMDS_ERR_CUSTOM;
+	}
+
 	ui_sb_msg(eval_result);
 	free(eval_result);
 	return 1;
@@ -2190,7 +2294,7 @@ edit_cmd(const cmd_info_t *cmd_info)
 		char full_path[PATH_MAX + 1];
 		get_full_path_of(entry, sizeof(full_path), full_path);
 
-		if(path_exists(full_path, DEREF) && !path_exists(full_path, NODEREF))
+		if(!path_exists(full_path, DEREF) && path_exists(full_path, NODEREF))
 		{
 			show_error_msgf("Access error",
 					"Can't access destination of link \"%s\". It might be broken.",
@@ -2273,10 +2377,10 @@ static int
 exe_cmd(const cmd_info_t *cmd_info)
 {
 	int result = 1;
-	char *const eval_result = try_eval_arglist(cmd_info);
+	char *const eval_result = try_eval_args(cmd_info);
 	if(eval_result != NULL)
 	{
-		result = exec_commands(eval_result, curr_view, CIT_COMMAND);
+		result = cmds_dispatch(eval_result, curr_view, CIT_COMMAND);
 		free(eval_result);
 	}
 	return result != 0;
@@ -2286,18 +2390,18 @@ exe_cmd(const cmd_info_t *cmd_info)
  * Returns pointer to newly allocated string, which should be freed by caller,
  * or NULL on error. */
 static char *
-try_eval_arglist(const cmd_info_t *cmd_info)
+try_eval_args(const cmd_info_t *cmd_info)
 {
 	char *eval_result;
 	const char *error_pos = NULL;
 
 	if(cmd_info->argc == 0)
 	{
-		return NULL;
+		return strdup("");
 	}
 
 	vle_tb_clear(vle_err);
-	eval_result = eval_arglist(cmd_info->raw_args, &error_pos);
+	eval_result = cmds_eval_args(cmd_info->raw_args, &error_pos);
 
 	if(eval_result == NULL)
 	{
@@ -2321,7 +2425,7 @@ file_cmd(const cmd_info_t *cmd_info)
 	if(rn_open_with_match(curr_view, cmd_info->argv[0], cmd_info->bg) != 0)
 	{
 		ui_sb_err("Can't find associated program with requested beginning");
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	return 0;
@@ -2372,8 +2476,7 @@ add_assoc(const cmd_info_t *cmd_info, int viewer, int for_x)
 	matchers = matchers_list(cmd_info->argv[0], &nmatchers);
 	if(matchers == NULL)
 	{
-		ui_sb_err("Not enough memory.");
-		return 1;
+		return CMDS_ERR_NO_MEM;
 	}
 
 	for(i = 0; i < nmatchers; ++i)
@@ -2385,7 +2488,7 @@ add_assoc(const cmd_info_t *cmd_info, int viewer, int for_x)
 			ui_sb_errf("Wrong pattern (%s): %s", matchers[i], error);
 			free(error);
 			free_string_array(matchers, nmatchers);
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 
 		if(viewer)
@@ -2527,7 +2630,7 @@ set_view_filter(view_t *view, const char filter[], const char fallback[],
 	{
 		ui_sb_errf("Name filter not set: %s", error);
 		free(error);
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	view->invert = invert;
@@ -2556,7 +2659,7 @@ find_cmd(const cmd_info_t *cmd_info)
 	else if(cmds_state.find.last_args == NULL)
 	{
 		ui_sb_err("Nothing to repeat");
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	return show_find_menu(curr_view, cmds_state.find.includes_path,
@@ -2569,11 +2672,11 @@ finish_cmd(const cmd_info_t *cmd_info)
 	if(curr_stats.sourcing_state != SOURCING_PROCESSING)
 	{
 		ui_sb_err(":finish used outside of a sourced file");
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	curr_stats.sourcing_state = SOURCING_FINISHING;
-	commands_scope_escape();
+	cmds_scope_escape();
 	return 0;
 }
 
@@ -2592,13 +2695,13 @@ goto_path_cmd(const cmd_info_t *cmd_info)
 	if(is_root_dir(abs_path))
 	{
 		ui_sb_errf("Can't navigate to root directory: %s", abs_path);
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	if(!path_exists(abs_path, NODEREF))
 	{
 		ui_sb_errf("Path doesn't exist: %s", abs_path);
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	fname = strdup(get_last_path_component(abs_path));
@@ -2623,7 +2726,7 @@ grep_cmd(const cmd_info_t *cmd_info)
 	else if(last_args == NULL)
 	{
 		ui_sb_err("Nothing to repeat");
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	inv = last_invert;
@@ -2664,7 +2767,7 @@ help_cmd(const cmd_info_t *cmd_info)
 		if(cmd_info->argc != 0)
 		{
 			ui_sb_err("No arguments are allowed when 'vimhelp' option is off");
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 
 		char help_file[PATH_MAX + 1];
@@ -2743,7 +2846,7 @@ highlight_clear(const cmd_info_t *cmd_info)
 		if(!cs_del_file_hi(cmd_info->argv[1]))
 		{
 			ui_sb_errf("No such group: %s", cmd_info->argv[1]);
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 
 		ui_invalidate_cs(curr_stats.cs);
@@ -2857,7 +2960,7 @@ highlight_group(const cmd_info_t *cmd_info)
 	if(group_id < 0)
 	{
 		ui_sb_errf("Highlight group not found: %s", cmd_info->argv[0]);
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	color = &curr_stats.cs->color[group_id];
@@ -2989,12 +3092,12 @@ parse_file_highlight(const cmd_info_t *cmd_info, col_attr_t *color)
 		if(equal == NULL)
 		{
 			ui_sb_errf("Missing equal sign in \"%s\"", arg);
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 		if(equal[1] == '\0')
 		{
 			ui_sb_errf("Missing argument: %s", arg);
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 
 		copy_str(arg_name, MIN(sizeof(arg_name), (size_t)(equal - arg + 1)), arg);
@@ -3003,6 +3106,7 @@ parse_file_highlight(const cmd_info_t *cmd_info, col_attr_t *color)
 		{
 			if(try_parse_cterm_color(equal + 1, 0, color) != 0)
 			{
+				/* Color not supported by the current terminal is not a hard error. */
 				return 1;
 			}
 		}
@@ -3010,6 +3114,7 @@ parse_file_highlight(const cmd_info_t *cmd_info, col_attr_t *color)
 		{
 			if(try_parse_cterm_color(equal + 1, 1, color) != 0)
 			{
+				/* Color not supported by the current terminal is not a hard error. */
 				return 1;
 			}
 		}
@@ -3018,6 +3123,7 @@ parse_file_highlight(const cmd_info_t *cmd_info, col_attr_t *color)
 			int value;
 			if(try_parse_gui_color(equal + 1, &value) != 0)
 			{
+				/* Color not supported by the current terminal is not a hard error. */
 				return 1;
 			}
 
@@ -3029,6 +3135,7 @@ parse_file_highlight(const cmd_info_t *cmd_info, col_attr_t *color)
 			int value;
 			if(try_parse_gui_color(equal + 1, &value) != 0)
 			{
+				/* Color not supported by the current terminal is not a hard error. */
 				return 1;
 			}
 
@@ -3042,7 +3149,7 @@ parse_file_highlight(const cmd_info_t *cmd_info, col_attr_t *color)
 			if((attrs = get_attrs(equal + 1, &combine_attrs)) == -1)
 			{
 				ui_sb_errf("Illegal argument: %s", equal + 1);
-				return 1;
+				return CMDS_ERR_CUSTOM;
 			}
 
 			if(strcmp(arg_name, "cterm") == 0)
@@ -3066,7 +3173,7 @@ parse_file_highlight(const cmd_info_t *cmd_info, col_attr_t *color)
 		else
 		{
 			ui_sb_errf("Illegal argument: %s", arg);
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 	}
 
@@ -3301,20 +3408,25 @@ if_cmd(const cmd_info_t *cmd_info)
 static int
 eval_if_condition(const cmd_info_t *cmd_info)
 {
-	var_t condition;
+	vle_tb_clear(vle_err);
+
 	int result;
 
-	vle_tb_clear(vle_err);
-	if(parse(cmd_info->args, 1, &condition) != PE_NO_ERROR)
+	parsing_result_t parsing_result =
+		vle_parser_eval(cmd_info->args, /*interactive=*/1);
+	if(parsing_result.error != PE_NO_ERROR)
 	{
 		vle_tb_append_linef(vle_err, "%s: %s", "Invalid expression",
 				cmd_info->args);
 		ui_sb_err(vle_tb_get_data(vle_err));
-		return -1;
+		result = CMDS_ERR_CUSTOM;
+	}
+	else
+	{
+		result = var_to_bool(parsing_result.value);
 	}
 
-	result = var_to_bool(condition);
-	var_free(condition);
+	var_free(parsing_result.value);
 	return result;
 }
 
@@ -3403,7 +3515,7 @@ jobs_cmd(const cmd_info_t *cmd_info)
 static int
 keepsel_cmd(const cmd_info_t *cmd_info)
 {
-	return cmds_exec(curr_view, cmd_info->args, /*menu=*/0, /*keep_sel=*/1);
+	return cmds_exec(cmd_info->args, curr_view, /*menu=*/0, /*keep_sel=*/1);
 }
 
 static int
@@ -3413,7 +3525,7 @@ let_cmd(const cmd_info_t *cmd_info)
 	if(let_variables(cmd_info->args) != 0)
 	{
 		ui_sb_err(vle_tb_get_data(vle_err));
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 	else if(*vle_tb_get_data(vle_err) != '\0')
 	{
@@ -3434,7 +3546,7 @@ locate_cmd(const cmd_info_t *cmd_info)
 	else if(last_args == NULL)
 	{
 		ui_sb_err("Nothing to repeat");
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 	return show_locate_menu(curr_view, last_args) != 0;
 }
@@ -3498,7 +3610,7 @@ mark_cmd(const cmd_info_t *cmd_info)
 		if(!marks_is_empty(curr_view, mark))
 		{
 			ui_sb_errf("Mark isn't empty: %c", mark);
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 	}
 
@@ -3516,7 +3628,7 @@ mark_cmd(const cmd_info_t *cmd_info)
 	{
 		free(expanded_path);
 		ui_sb_err("Expected full path to the directory");
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	if(cmd_info->argc == 2)
@@ -3680,7 +3792,7 @@ cpmv_cmd(const cmd_info_t *cmd_info, int move)
 		if(argc > 0)
 		{
 			ui_sb_err("No positional arguments are allowed if you use \"?\"");
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 
 		if(cmd_info->bg)
@@ -3761,6 +3873,10 @@ normal_cmd(const cmd_info_t *cmd_info)
 		return 0;
 	}
 
+	vle_mode_t old_primary_mode = vle_mode_get_primary();
+	vle_mode_t old_current_mode = vle_mode_get();
+	vle_mode_set(NORMAL_MODE, VMT_PRIMARY);
+
 	if(cmd_info->emark)
 	{
 		(void)vle_keys_exec_timed_out_no_remap(wide);
@@ -3769,14 +3885,23 @@ normal_cmd(const cmd_info_t *cmd_info)
 	{
 		(void)vle_keys_exec_timed_out(wide);
 	}
+	free(wide);
 
-	/* Force leaving command-line mode if the wide contains unfinished ":". */
-	if(vle_mode_is(CMDLINE_MODE))
+	/* Force leaving command-line mode if the input contains unfinished ":". */
+	if(modes_is_cmdline_like())
 	{
 		(void)vle_keys_exec_timed_out(WK_C_c);
 	}
 
-	free(wide);
+	/* Don't restore the mode if input sequence led to a mode change as it was
+	 * probably the intention of the user. */
+	if(vle_mode_is(NORMAL_MODE))
+	{
+		/* Relative order of the calls is important. */
+		vle_mode_set(old_primary_mode, VMT_PRIMARY);
+		vle_mode_set(old_current_mode, VMT_SECONDARY);
+	}
+
 	cmds_preserve_selection();
 	return 0;
 }
@@ -3805,7 +3930,7 @@ plugin_cmd(const cmd_info_t *cmd_info)
 			return CMDS_ERR_TRAILING_CHARS;
 		}
 
-		plugs_load(curr_stats.plugs, cfg.config_dir);
+		plugs_load(curr_stats.plugs, curr_stats.plugins_dirs);
 		return 0;
 	}
 
@@ -3855,7 +3980,7 @@ pushd_cmd(const cmd_info_t *cmd_info)
 		if(dir_stack_swap() != 0)
 		{
 			ui_sb_err("No other directories");
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 		return 0;
 	}
@@ -3926,6 +4051,83 @@ redraw_cmd(const cmd_info_t *cmd_info)
 	return 0;
 }
 
+/* Opens external editor to edit specified register contents. */
+static int
+regedit_cmd(const cmd_info_t *cmd_info)
+{
+	int reg_name = DEFAULT_REG_NAME;
+	if(cmd_info->argc > 0)
+	{
+		if(strlen(cmd_info->argv[0]) != 1)
+		{
+			ui_sb_errf("Invalid argument: %s", cmd_info->argv[0]);
+			return CMDS_ERR_CUSTOM;
+		}
+		reg_name = tolower(cmd_info->argv[0][0]);
+	}
+	if(reg_name == BLACKHOLE_REG_NAME)
+	{
+		ui_sb_err("Cannot modify blackhole register.");
+		return CMDS_ERR_CUSTOM;
+	}
+
+	regs_sync_from_shared_memory();
+	const reg_t *reg = regs_find(reg_name);
+	if(reg == NULL)
+	{
+		ui_sb_err("Register with given name does not exist.");
+		return CMDS_ERR_CUSTOM;
+	}
+
+	char tmp_fname[PATH_MAX + 1];
+	FILE *file = make_file_in_tmp("vifm.regedit", 0600, /*auto_delete=*/0,
+			tmp_fname, sizeof(tmp_fname));
+	if(file == NULL)
+	{
+		ui_sb_err("Couldn't write register content into external file.");
+		return CMDS_ERR_CUSTOM;
+	}
+
+	write_lines_to_file(file, reg->files, reg->nfiles);
+	fclose(file);
+
+	/* Forking is disabled because in some cases process can return value
+	 * before any changes will be written and editor will be closed. */
+	const int edit_result = vim_view_file(tmp_fname, 1, 1, /*allow_forking=*/0);
+	if(edit_result != 0)
+	{
+		ui_sb_err("Register content edition went unsuccessful.");
+		return CMDS_ERR_CUSTOM;
+	}
+
+	int read_lines;
+	char **edited_content = read_file_of_lines(tmp_fname, &read_lines);
+	int error = errno;
+	unlink(tmp_fname);
+	if(edited_content == NULL)
+	{
+		ui_sb_errf("Couldn't read edited register's content: %s", strerror(error));
+		return CMDS_ERR_CUSTOM;
+	}
+
+	/* Normalize paths.  Not done in regs_set() as it doesn't need to know about
+	 * current directory. */
+	int i;
+	const char *base_dir = flist_get_dir(curr_view);
+	for(i = 0; i < read_lines; ++i)
+	{
+		char canonic[PATH_MAX + 1];
+		to_canonic_path(edited_content[i], base_dir, canonic, sizeof(canonic));
+		replace_string(&edited_content[i], canonic);
+	}
+
+	regs_set(reg_name, edited_content, read_lines);
+	free_string_array(edited_content, read_lines);
+
+	regs_sync_to_shared_memory();
+	return 0;
+}
+
 /* Displays menu listing contents of registers (all or just specified ones). */
 static int
 registers_cmd(const cmd_info_t *cmd_info)
@@ -4034,7 +4236,7 @@ link_cmd(const cmd_info_t *cmd_info, int absolute)
 		if(argc > 0)
 		{
 			ui_sb_err("No positional arguments are allowed if you use \"?\"");
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 		return fops_cpmv(curr_view, NULL, -1, op, flags) != 0;
 	}
@@ -4194,7 +4396,7 @@ session_cmd(const cmd_info_t *cmd_info)
 	if(contains_slash(session_name))
 	{
 		ui_sb_err("Session name can't include path separators");
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	if(strcmp(session_name, "-") == 0)
@@ -4202,12 +4404,12 @@ session_cmd(const cmd_info_t *cmd_info)
 		if(is_null_or_empty(curr_stats.last_session))
 		{
 			ui_sb_err("No previous session");
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 		if(!sessions_exists(curr_stats.last_session))
 		{
 			ui_sb_err("Previous session doesn't exist");
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 
 		session_name = curr_stats.last_session;
@@ -4258,7 +4460,7 @@ switch_to_a_session(const char session_name[])
 		{
 			ui_sb_err("Session switching has failed, no active session");
 		}
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	ui_sb_msgf("Loaded session: %s", sessions_current());
@@ -4352,21 +4554,23 @@ source_cmd(const cmd_info_t *cmd_info)
 {
 	int ret = 0;
 	char *path = expand_tilde(cmd_info->argv[0]);
+
 	if(!path_exists(path, DEREF))
 	{
-		ui_sb_errf("File doesn't exist: %s", cmd_info->argv[0]);
-		ret = 1;
+		ui_sb_errf("Sourced file doesn't exist: %s", cmd_info->argv[0]);
+		ret = CMDS_ERR_CUSTOM;
 	}
-	if(os_access(path, R_OK) != 0)
+	else if(os_access(path, R_OK) != 0)
 	{
-		ui_sb_errf("File isn't readable: %s", cmd_info->argv[0]);
-		ret = 1;
+		ui_sb_errf("Sourced file isn't readable: %s", cmd_info->argv[0]);
+		ret = CMDS_ERR_CUSTOM;
 	}
-	if(cfg_source_file(path) != 0)
+	else if(cfg_source_file(path) != 0)
 	{
 		ui_sb_errf("Error sourcing file: %s", cmd_info->argv[0]);
-		ret = 1;
+		ret = CMDS_ERR_CUSTOM;
 	}
+
 	free(path);
 	return ret;
 }
@@ -4446,7 +4650,7 @@ substitute_cmd(const cmd_info_t *cmd_info)
 	if(is_null_or_empty(last_pattern))
 	{
 		ui_sb_err("No previous pattern");
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	flist_set_marking(curr_view, 0);
@@ -4494,7 +4698,7 @@ sync_selectively(const cmd_info_t *cmd_info)
 	if(parse_sync_properties(cmd_info, &location, &cursor_pos, &local_options,
 				&filters, &filelist, &tree) != 0)
 	{
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	if(!cv_tree(curr_view->custom.type))
@@ -4642,7 +4846,7 @@ sync_location(const char path[], int cv, int sync_cursor_pos, int sync_filters,
 					get_current_file_name(curr_view), 1);
 
 			other_view->top_line = MAX(0, curr_view->list_pos - shift);
-			(void)consider_scroll_offset(other_view);
+			(void)fview_enforce_scroll_offset(other_view);
 		}
 
 		flist_hist_save(other_view);
@@ -4719,7 +4923,7 @@ tabnew_cmd(const cmd_info_t *cmd_info)
 	if(cfg.pane_tabs && curr_view->custom.type == CV_DIFF)
 	{
 		ui_sb_err("Switching tab of single pane would drop comparison");
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	const char *path = NULL;
@@ -4751,7 +4955,7 @@ tabnew_cmd(const cmd_info_t *cmd_info)
 	if(tabs_new(NULL, path) != 0)
 	{
 		ui_sb_err("Failed to open a new tab");
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 	return 0;
 }
@@ -4832,7 +5036,7 @@ tr_cmd(const cmd_info_t *cmd_info)
 	if(cmd_info->argv[0][0] == '\0' || cmd_info->argv[1][0] == '\0')
 	{
 		ui_sb_err("Empty argument");
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	pl = strlen(cmd_info->argv[0]);
@@ -4841,7 +5045,7 @@ tr_cmd(const cmd_info_t *cmd_info)
 	if(pl < sl)
 	{
 		ui_sb_err("Second argument cannot be longer");
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 	else if(pl > sl)
 	{
@@ -4907,7 +5111,7 @@ parse_tree_properties(const cmd_info_t *cmd_info, int *depth)
 			if(*endptr != '\0' || value < 1)
 			{
 				ui_sb_errf("Invalid depth: %s", arg);
-				return 1;
+				return CMDS_ERR_CUSTOM;
 			}
 
 			*depth = value - 1;
@@ -4915,7 +5119,7 @@ parse_tree_properties(const cmd_info_t *cmd_info, int *depth)
 		else
 		{
 			ui_sb_errf("Invalid argument: %s", arg);
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 	}
 
@@ -4935,7 +5139,7 @@ unlet_cmd(const cmd_info_t *cmd_info)
 	if(unlet_variables(cmd_info->args) != 0 && !cmd_info->emark)
 	{
 		ui_sb_err(vle_tb_get_data(vle_err));
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 	return 0;
 }
@@ -4978,7 +5182,7 @@ unmap_cmd(const cmd_info_t *cmd_info)
 	{
 		ui_sb_err("Error while unmapping keys");
 	}
-	return result != 0;
+	return (result != 0 ? CMDS_ERR_CUSTOM : 0);
 }
 
 /* Unselects files that match passed in expression or range. */
@@ -5020,7 +5224,7 @@ view_cmd(const cmd_info_t *cmd_info)
 
 	if((!curr_stats.preview.on || cmd_info->emark) && !qv_can_show())
 	{
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 	if(curr_stats.preview.on && cmd_info->emark)
 	{
@@ -5149,7 +5353,7 @@ do_split(const cmd_info_t *cmd_info, SPLIT orientation)
 	if(cmd_info->emark && cmd_info->argc != 0)
 	{
 		ui_sb_err("No arguments are allowed if you use \"!\"");
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 
 	if(cmd_info->emark)
@@ -5193,7 +5397,7 @@ do_unmap(const char keys[], int mode)
 	if(result != 0)
 	{
 		ui_sb_err("No such mapping");
-		return 1;
+		return CMDS_ERR_CUSTOM;
 	}
 	return 0;
 }
@@ -5305,7 +5509,7 @@ winrun(view_t *view, const char cmd[])
 	/* :winrun and :windo should be able to set settings separately for each
 	 * window. */
 	curr_stats.global_local_settings = 0;
-	result = exec_commands(cmd, curr_view, CIT_COMMAND);
+	result = cmds_dispatch(cmd, curr_view, CIT_COMMAND);
 	curr_stats.global_local_settings = prev_global_local_settings;
 
 	ui_view_unpick(view, tmp_curr, tmp_other);
@@ -5461,15 +5665,15 @@ usercmd_cmd(const cmd_info_t *cmd_info)
 
 	if(expanded_com[0] == ':')
 	{
-		commands_scope_start();
+		cmds_scope_start();
 
-		int sm = exec_commands(expanded_com, curr_view, CIT_COMMAND);
+		int sm = cmds_dispatch(expanded_com, curr_view, CIT_COMMAND);
 		free(expanded_com);
 
-		if(commands_scope_finish() != 0)
+		if(cmds_scope_finish() != 0)
 		{
 			ui_sb_err("Unmatched if-else-endif");
-			return 1;
+			return CMDS_ERR_CUSTOM;
 		}
 
 		return sm != 0;
@@ -5499,7 +5703,7 @@ usercmd_cmd(const cmd_info_t *cmd_info)
 	else if(starts_with_lit(expanded_com, "filter") &&
 			char_is_one_of(" !/", expanded_com[6]))
 	{
-		save_msg = exec_command(expanded_com, curr_view, CIT_COMMAND);
+		save_msg = cmds_dispatch1(expanded_com, curr_view, CIT_COMMAND);
 		external = 0;
 	}
 	else if(expanded_com[0] == '!')
@@ -5526,13 +5730,14 @@ usercmd_cmd(const cmd_info_t *cmd_info)
 	}
 	else if(expanded_com[0] == '/')
 	{
-		exec_command(expanded_com + 1, curr_view, CIT_FSEARCH_PATTERN);
+		modnorm_set_search_attrs(/*count=*/1, /*last_search_backward=*/0);
+		cmds_dispatch1(expanded_com + 1, curr_view, CIT_FSEARCH_PATTERN);
 		cmds_preserve_selection();
 		external = 0;
 	}
 	else if(expanded_com[0] == '=')
 	{
-		exec_command(expanded_com + 1, curr_view, CIT_FILTER_PATTERN);
+		cmds_dispatch1(expanded_com + 1, curr_view, CIT_FILTER_PATTERN);
 		ui_view_schedule_reload(curr_view);
 		cmds_preserve_selection();
 		external = 0;
diff --git a/src/cmd_handlers.h b/src/cmd_handlers.h
index c7a9821..5ca6026 100644
--- a/src/cmd_handlers.h
+++ b/src/cmd_handlers.h
@@ -31,6 +31,8 @@
 enum
 {
 	COM_CDS = NO_COMPLETION_BOUNDARY,
+	COM_AMAP,
+	COM_ANOREMAP,
 	COM_CMAP,
 	COM_CNOREMAP,
 	COM_COMMAND,
diff --git a/src/compare.c b/src/compare.c
index 1747946..27082a3 100644
--- a/src/compare.c
+++ b/src/compare.c
@@ -43,9 +43,28 @@
 #include "utils/utils.h"
 #include "filelist.h"
 #include "filtering.h"
+#include "flist_sel.h"
+#include "fops_common.h"
 #include "fops_cpmv.h"
 #include "fops_misc.h"
 #include "running.h"
+#include "undo.h"
+
+/*
+ * Optimization for content-based matching.
+ *
+ * At first fingerprint uses only size of the file, then it's looked up to see
+ * if there is any other file of the same size.  If there isn't, store compare
+ * record by that fingerprint.
+ *
+ * If there is, two cases are possible:
+ *   - there is only one conflicting file and its contents fingerprint wasn't
+ *     yet computed:
+ *       * compute contents fingerprint for that file and insert it
+ *       * compute contents fingerprint for current file and insert it
+ *   - there is more than one conflicting file:
+ *       * compute contents fingerprint for current file and insert it
+ */
 
 /* This is the only unit that uses xxhash, so import it directly here. */
 #define XXH_PRIVATE_API
@@ -62,37 +81,50 @@ typedef struct compare_record_t
 {
 	char *path;                    /* Full path to file with sample content. */
 	int id;                        /* Chosen id. */
-	struct compare_record_t *next; /* Next entry in the list. */
+	int is_partial;                /* Shows that fingerprinting was lazy. */
+	struct compare_record_t *next; /* Next entry in the list of conflicts. */
 }
 compare_record_t;
 
 static void make_unique_lists(entries_t curr, entries_t other);
 static void leave_only_dups(entries_t *curr, entries_t *other);
 static int is_not_duplicate(view_t *view, const dir_entry_t *entry, void *arg);
-static void fill_side_by_side(entries_t curr, entries_t other, int group_paths);
+static void fill_side_by_side_by_paths(entries_t curr, entries_t other,
+		int flags, compare_stats_t *stats);
+static void fill_side_by_side_by_ids(entries_t curr, entries_t other, int flags,
+		compare_stats_t *stats);
+static int compare_entries(dir_entry_t *curr, dir_entry_t *other, int flags);
+static void put_side_by_side_pair(dir_entry_t *curr, dir_entry_t *other,
+		int flags, compare_stats_t *stats);
 static int id_sorter(const void *first, const void *second);
 static void put_or_free(view_t *view, dir_entry_t *entry, int id, int take);
 static entries_t make_diff_list(trie_t *trie, view_t *view, int *next_id,
-		CompareType ct, int skip_empty, int dups_only);
+		CompareType ct, int dups_only, int flags);
 static void list_view_entries(const view_t *view, strlist_t *list);
 static int append_valid_nodes(const char name[], int valid,
 		const void *parent_data, void *data, void *arg);
 static void list_files_recursively(const view_t *view, const char path[],
-		int skip_dot_files, strlist_t *list);
+		int skip_dot_files, int flags, strlist_t *list);
 static char * get_file_fingerprint(const char path[], const dir_entry_t *entry,
-		CompareType ct);
+		CompareType ct, int flags, int lazy);
 static char * get_contents_fingerprint(const char path[],
-		const dir_entry_t *entry);
-static int get_file_id(trie_t *trie, const char path[],
-		const char fingerprint[], int *id, CompareType ct);
+		unsigned long long size);
+static int add_file_to_diff(trie_t *trie, const char path[], dir_entry_t *entry,
+		CompareType ct, int dups_only, int flags, int *next_id);
 static int files_are_identical(const char a[], const char b[]);
 static void put_file_id(trie_t *trie, const char path[],
-		const char fingerprint[], int id, CompareType ct);
+		const char fingerprint[], int id, int is_partial, CompareType ct);
 static void free_compare_records(void *ptr);
+static void compare_move_entry(ops_t *ops, view_t *from, view_t *to, int idx);
 
 int
-compare_two_panes(CompareType ct, ListType lt, int group_paths, int skip_empty)
+compare_two_panes(CompareType ct, ListType lt, int flags)
 {
+	assert((flags & (CF_IGNORE_CASE | CF_RESPECT_CASE)) !=
+			(CF_IGNORE_CASE | CF_RESPECT_CASE) && "Wrong combination of flags.");
+
+	const int group_paths = flags & CF_GROUP_PATHS;
+
 	/* We don't compare lists of files, so skip the check if at least one of the
 	 * views is a custom one. */
 	if(!flist_custom_active(&lwin) && !flist_custom_active(&rwin) &&
@@ -108,9 +140,8 @@ compare_two_panes(CompareType ct, ListType lt, int group_paths, int skip_empty)
 	trie_t *const trie = trie_create(&free_compare_records);
 	ui_cancellation_push_on();
 
-	curr = make_diff_list(trie, curr_view, &next_id, ct, skip_empty, 0);
-	other = make_diff_list(trie, other_view, &next_id, ct, skip_empty,
-			lt == LT_DUPS);
+	curr = make_diff_list(trie, curr_view, &next_id, ct, /*dups_only=*/0, flags);
+	other = make_diff_list(trie, other_view, &next_id, ct, lt == LT_DUPS, flags);
 
 	ui_cancellation_pop();
 	trie_free(trie);
@@ -120,8 +151,8 @@ compare_two_panes(CompareType ct, ListType lt, int group_paths, int skip_empty)
 
 	if(ui_cancellation_requested())
 	{
-		free_dir_entries(curr_view, &curr.entries, &curr.nentries);
-		free_dir_entries(other_view, &other.entries, &other.nentries);
+		free_dir_entries(&curr.entries, &curr.nentries);
+		free_dir_entries(&other.entries, &other.nentries);
 		ui_sb_msg("Comparison has been cancelled");
 		return 1;
 	}
@@ -149,7 +180,21 @@ compare_two_panes(CompareType ct, ListType lt, int group_paths, int skip_empty)
 	flist_custom_start(curr_view, lt == LT_ALL ? "diff" : "dups diff");
 	flist_custom_start(other_view, lt == LT_ALL ? "diff" : "dups diff");
 
-	fill_side_by_side(curr, other, group_paths);
+	compare_stats_t stats = {};
+
+	if(group_paths)
+	{
+		fill_side_by_side_by_paths(curr, other, flags, &stats);
+	}
+	else
+	{
+		fill_side_by_side_by_ids(curr, other, flags, &stats);
+	}
+
+	/* Entries' data has been moved out of them, so need to free only the
+	 * lists. */
+	dynarray_free(curr.entries);
+	dynarray_free(other.entries);
 
 	if(flist_custom_finish(curr_view, CV_DIFF, 0) != 0)
 	{
@@ -165,8 +210,15 @@ compare_two_panes(CompareType ct, ListType lt, int group_paths, int skip_empty)
 	other_view->list_pos = 0;
 	curr_view->custom.diff_cmp_type = ct;
 	other_view->custom.diff_cmp_type = ct;
-	curr_view->custom.diff_path_group = group_paths;
-	other_view->custom.diff_path_group = group_paths;
+	curr_view->custom.diff_list_type = lt;
+	other_view->custom.diff_list_type = lt;
+	curr_view->custom.diff_cmp_flags = flags;
+	other_view->custom.diff_cmp_flags = flags;
+
+	/* Both views share the same stats, alternatively can put to status_t, but
+	 * then have to save/store per global tab. */
+	curr_view->custom.diff_stats = stats;
+	other_view->custom.diff_stats = stats;
 
 	assert(curr_view->list_rows == other_view->list_rows &&
 			"Diff views must be in sync!");
@@ -206,11 +258,11 @@ make_unique_lists(entries_t curr, entries_t other)
 
 		while(j < curr.nentries && curr.entries[j].id == id)
 		{
-			fentry_free(curr_view, &curr.entries[j++]);
+			fentry_free(&curr.entries[j++]);
 		}
 		while(i < other.nentries && other.entries[i].id == id)
 		{
-			fentry_free(other_view, &other.entries[i++]);
+			fentry_free(&other.entries[i++]);
 		}
 		/* Want to revisit this entry on the next iteration of the loop. */
 		--i;
@@ -304,32 +356,74 @@ is_not_duplicate(view_t *view, const dir_entry_t *entry, void *arg)
 	return entry->id != -1;
 }
 
-/* Composes side-by-side comparison of files in two views. */
+/* Composes side-by-side comparison of files in two views centered around their
+ * relative paths. */
 static void
-fill_side_by_side(entries_t curr, entries_t other, int group_paths)
+fill_side_by_side_by_paths(entries_t curr, entries_t other, int flags,
+		compare_stats_t *stats)
+{
+	int i = 0;
+	int j = 0;
+
+	while(i < curr.nentries || j < other.nentries)
+	{
+		int cmp;
+
+		if(i < curr.nentries && j < other.nentries)
+		{
+			cmp = compare_entries(&curr.entries[i], &other.entries[j], flags);
+		}
+		else
+		{
+			cmp = (i < curr.nentries ? -1 : 1);
+		}
+
+		if(cmp == 0)
+		{
+			put_side_by_side_pair(&curr.entries[i++], &other.entries[j++], flags,
+					stats);
+		}
+		else if(cmp < 0)
+		{
+			put_side_by_side_pair(&curr.entries[i++], NULL, flags, stats);
+		}
+		else
+		{
+			put_side_by_side_pair(NULL, &other.entries[j++], flags, stats);
+		}
+	}
+}
+
+/* Composes side-by-side comparison of files in two views that is guided by
+ * comparison ids and minimizes edit script. */
+static void
+fill_side_by_side_by_ids(entries_t curr, entries_t other, int flags,
+		compare_stats_t *stats)
 {
 	enum { UP, LEFT, DIAG };
 
 	int i, j;
-	/* Describes results of solving sub-problems. */
-	int (*d)[other.nentries + 1] =
-		reallocarray(NULL, curr.nentries + 1, sizeof(*d));
+
+	/* Describes results of solving sub-problems.  Only two rows of the full
+	 * table. */
+	int (*d)[other.nentries + 1] = reallocarray(NULL, 2, sizeof(*d));
 	/* Describes paths (backtracking handles ambiguity badly). */
 	char (*p)[other.nentries + 1] =
 		reallocarray(NULL, curr.nentries + 1, sizeof(*p));
 
 	for(i = 0; i <= curr.nentries; ++i)
 	{
+		int row = i%2;
 		for(j = 0; j <= other.nentries; ++j)
 		{
 			if(i == 0)
 			{
-				d[i][j] = j;
+				d[row][j] = j;
 				p[i][j] = LEFT;
 			}
 			else if(j == 0)
 			{
-				d[i][j] = i;
+				d[row][j] = i;
 				p[i][j] = UP;
 			}
 			else
@@ -337,14 +431,12 @@ fill_side_by_side(entries_t curr, entries_t other, int group_paths)
 				const dir_entry_t *centry = &curr.entries[curr.nentries - i];
 				const dir_entry_t *oentry = &other.entries[other.nentries - j];
 
-				d[i][j] = MIN(d[i - 1][j] + 1, d[i][j - 1] + 1);
-				p[i][j] = d[i][j] == d[i - 1][j] + 1 ? UP : LEFT;
+				d[row][j] = MIN(d[1 - row][j] + 1, d[row][j - 1] + 1);
+				p[i][j] = d[row][j] == d[1 - row][j] + 1 ? UP : LEFT;
 
-				if((centry->id == oentry->id ||
-							(group_paths && stroscmp(centry->name, oentry->name) == 0)) &&
-						d[i - 1][j - 1] <= d[i][j])
+				if(centry->id == oentry->id && d[1 - row][j - 1] <= d[row][j])
 				{
-					d[i][j] = d[i - 1][j - 1];
+					d[row][j] = d[1 - row][j - 1];
 					p[i][j] = DIAG;
 				}
 			}
@@ -357,37 +449,153 @@ fill_side_by_side(entries_t curr, entries_t other, int group_paths)
 	{
 		switch(p[i][j])
 		{
-			dir_entry_t *e;
-
 			case UP:
-				e = &curr.entries[curr.nentries - 1 - --i];
-				flist_custom_put(curr_view, e);
-				flist_custom_add_separator(other_view, e->id);
+				put_side_by_side_pair(&curr.entries[curr.nentries - i--], NULL, flags,
+						stats);
 				break;
 			case LEFT:
-				e = &other.entries[other.nentries - 1 - --j];
-				flist_custom_put(other_view, e);
-				flist_custom_add_separator(curr_view, e->id);
+				put_side_by_side_pair(NULL, &other.entries[other.nentries - j--], flags,
+						stats);
 				break;
 			case DIAG:
-				flist_custom_put(curr_view, &curr.entries[curr.nentries - 1 - --i]);
-				flist_custom_put(other_view, &other.entries[other.nentries - 1 - --j]);
+				put_side_by_side_pair(&curr.entries[curr.nentries - i--],
+						&other.entries[other.nentries - j--], flags, stats);
 				break;
 		}
 	}
 
 	free(d);
 	free(p);
+}
 
-	/* Entries' data has been moved out of them, so need to free only the
-	 * lists. */
-	dynarray_free(curr.entries);
-	dynarray_free(other.entries);
+/* Adds an entry per side.  Either curr or other can be NULL. */
+static void
+put_side_by_side_pair(dir_entry_t *curr, dir_entry_t *other, int flags,
+		compare_stats_t *stats)
+{
+	/* Integer type to avoid warning about unhandled cases in switch(). */
+	int flag;
+
+	if(other == NULL)
+	{
+		flag = (curr_view == &lwin ? CF_SHOW_UNIQUE_LEFT : CF_SHOW_UNIQUE_RIGHT);
+	}
+	else if(curr == NULL)
+	{
+		flag = (other_view == &lwin ? CF_SHOW_UNIQUE_LEFT : CF_SHOW_UNIQUE_RIGHT);
+	}
+	else
+	{
+		flag = (curr->id == other->id ? CF_SHOW_IDENTICAL : CF_SHOW_DIFFERENT);
+	}
+
+	switch(flag)
+	{
+		case CF_SHOW_UNIQUE_LEFT:  ++stats->unique_left;  break;
+		case CF_SHOW_UNIQUE_RIGHT: ++stats->unique_right; break;
+		case CF_SHOW_IDENTICAL:    ++stats->identical;    break;
+		case CF_SHOW_DIFFERENT:    ++stats->different;    break;
+	}
+
+	if(flags & flag)
+	{
+		if(curr != NULL)
+		{
+			flist_custom_put(curr_view, curr);
+		}
+		else
+		{
+			flist_custom_add_separator(curr_view, other->id);
+		}
+
+		if(other != NULL)
+		{
+			flist_custom_put(other_view, other);
+		}
+		else
+		{
+			flist_custom_add_separator(other_view, curr->id);
+		}
+	}
+	else
+	{
+		if(curr != NULL)
+		{
+			fentry_free(curr);
+		}
+		if(other != NULL)
+		{
+			fentry_free(other);
+		}
+	}
+}
+
+/* Compares entries by their short paths.  Returns strcmp()-like result. */
+static int
+compare_entries(dir_entry_t *curr, dir_entry_t *other, int flags)
+{
+	char path_a[PATH_MAX + 1], path_b[PATH_MAX + 1];
+	get_full_path_of(curr, sizeof(path_a), path_a);
+	get_full_path_of(other, sizeof(path_b), path_b);
+
+	int case_sensitive;
+	if(flags & CF_IGNORE_CASE)
+	{
+		case_sensitive = 0;
+	}
+	else if(flags & CF_RESPECT_CASE)
+	{
+		case_sensitive = 1;
+	}
+	else
+	{
+		/* If at least one path is case-sensitive, don't ignore case.  Otherwise, we
+		 * would end up with multiple matching pairs of paths. */
+		case_sensitive = case_sensitive_paths(path_a)
+		              || case_sensitive_paths(path_b);
+	}
+
+	get_short_path_of(curr_view, curr, NF_NONE, 0, sizeof(path_a), path_a);
+	get_short_path_of(other_view, other, NF_NONE, 0, sizeof(path_b), path_b);
+
+	const char *a = path_a, *b = path_b;
+
+	char lower_a[PATH_MAX + 1], lower_b[PATH_MAX + 1];
+	if(!case_sensitive)
+	{
+		str_to_lower(path_a, lower_a, sizeof(lower_a));
+		str_to_lower(path_b, lower_b, sizeof(lower_b));
+
+		a = lower_a;
+		b = lower_b;
+	}
+
+	while(*a == *b && *a != '\0')
+	{
+		++a;
+		++b;
+	}
+
+	int cmp = *a - *b;
+
+	/* Correct result to reflect that a directory is smaller than a
+	 * non-directory. */
+	int dir_a = (strchr(a, '/') != NULL);
+	int dir_b = (strchr(b, '/') != NULL);
+	if(dir_a != dir_b)
+	{
+		cmp = (dir_a ? -1 : 1);
+	}
+
+	return cmp;
 }
 
 int
-compare_one_pane(view_t *view, CompareType ct, ListType lt, int skip_empty)
+compare_one_pane(view_t *view, CompareType ct, ListType lt, int flags)
 {
+	assert((flags & (CF_IGNORE_CASE | CF_RESPECT_CASE)) !=
+			(CF_IGNORE_CASE | CF_RESPECT_CASE) && "Wrong combination of flags.");
+
 	int i, dup_id;
 	view_t *other = (view == curr_view) ? other_view : curr_view;
 	const char *const title = (lt == LT_ALL)  ? "compare"
@@ -399,7 +607,7 @@ compare_one_pane(view_t *view, CompareType ct, ListType lt, int skip_empty)
 	trie_t *trie = trie_create(&free_compare_records);
 	ui_cancellation_push_on();
 
-	curr = make_diff_list(trie, view, &next_id, ct, skip_empty, 0);
+	curr = make_diff_list(trie, view, &next_id, ct, /*dups_only=*/0, flags);
 
 	ui_cancellation_pop();
 	trie_free(trie);
@@ -409,7 +617,7 @@ compare_one_pane(view_t *view, CompareType ct, ListType lt, int skip_empty)
 
 	if(ui_cancellation_requested())
 	{
-		free_dir_entries(view, &curr.entries, &curr.nentries);
+		free_dir_entries(&curr.entries, &curr.nentries);
 		ui_sb_msg("Comparison has been cancelled");
 		return 1;
 	}
@@ -467,6 +675,9 @@ compare_one_pane(view_t *view, CompareType ct, ListType lt, int skip_empty)
 		rn_leave(other, 1);
 	}
 
+	curr_view->custom.diff_cmp_flags = flags;
+	other_view->custom.diff_cmp_flags = flags;
+
 	view->list_pos = 0;
 	ui_view_schedule_redraw(view);
 	return 0;
@@ -494,7 +705,7 @@ put_or_free(view_t *view, dir_entry_t *entry, int id, int take)
 	}
 	else
 	{
-		fentry_free(view, entry);
+		fentry_free(entry);
 	}
 }
 
@@ -503,8 +714,10 @@ put_or_free(view_t *view, dir_entry_t *entry, int id, int take)
  * trie. */
 static entries_t
 make_diff_list(trie_t *trie, view_t *view, int *next_id, CompareType ct,
-		int skip_empty, int dups_only)
+		int dups_only, int flags)
 {
+	const int skip_empty = flags & CF_SKIP_EMPTY;
+
 	int i;
 	strlist_t files = {};
 	entries_t r = {};
@@ -518,55 +731,41 @@ make_diff_list(trie_t *trie, view_t *view, int *next_id, CompareType ct,
 	}
 	else
 	{
-		list_files_recursively(view, flist_get_dir(view), view->hide_dot, &files);
+		list_files_recursively(view, flist_get_dir(view), view->hide_dot, flags,
+				&files);
 	}
 
 	show_progress("Querying...", 0);
 	for(i = 0; i < files.nitems && !ui_cancellation_requested(); ++i)
 	{
 		int progress;
-		int existing_id;
-		char *fingerprint;
 		const char *const path = files.items[i];
 		dir_entry_t *const entry = entry_list_add(view, &r.entries, &r.nentries,
 				path);
-
-		if(skip_empty && entry->size == 0)
+		if(entry == NULL)
 		{
-			fentry_free(view, entry);
-			--r.nentries;
+			/* Maybe the file doesn't exist anymore, maybe we've lost access to it or
+			 * we're just out of memory. */
 			continue;
 		}
 
-		fingerprint = get_file_fingerprint(path, entry, ct);
-		/* In case we couldn't obtain fingerprint (e.g., comparing by contents and
-		 * files isn't readable), ignore the file and keep going. */
-		if(is_null_or_empty(fingerprint))
+		if(skip_empty && entry->size == 0)
 		{
-			free(fingerprint);
-			fentry_free(view, entry);
+			fentry_free(entry);
 			--r.nentries;
 			continue;
 		}
 
 		entry->tag = i;
-		if(get_file_id(trie, path, fingerprint, &existing_id, ct))
-		{
-			entry->id = existing_id;
-		}
-		else if(dups_only)
-		{
-			entry->id = -1;
-		}
-		else
+		entry->id = add_file_to_diff(trie, path, entry, ct, dups_only, flags,
+				next_id);
+
+		if(entry->id == -1)
 		{
-			entry->id = *next_id;
-			++*next_id;
-			put_file_id(trie, path, fingerprint, entry->id, ct);
+			fentry_free(entry);
+			--r.nentries;
 		}
 
-		free(fingerprint);
-
 		progress = (i*100)/files.nitems;
 		if(progress != last_progress)
 		{
@@ -629,18 +828,31 @@ append_valid_nodes(const char name[], int valid, const void *parent_data,
 /* Collects files under specified file system tree. */
 static void
 list_files_recursively(const view_t *view, const char path[],
-		int skip_dot_files, strlist_t *list)
+		int skip_dot_files, int flags, strlist_t *list)
 {
 	int i;
 
 	/* Obtain sorted list of files. */
 	int len;
-	char **lst = list_sorted_files(path, &len);
+	char **lst = list_all_files(path, &len);
 	if(len < 0)
 	{
 		return;
 	}
 
+	if(flags & CF_IGNORE_CASE)
+	{
+		safe_qsort(lst, len, sizeof(*lst), &strcasesorter);
+	}
+	else if(flags & CF_RESPECT_CASE)
+	{
+		safe_qsort(lst, len, sizeof(*lst), &strsorter);
+	}
+	else
+	{
+		safe_qsort(lst, len, sizeof(*lst), &strossorter);
+	}
+
 	/* Visit all subdirectories ignoring symbolic links to directories. */
 	for(i = 0; i < len && !ui_cancellation_requested(); ++i)
 	{
@@ -663,7 +875,7 @@ list_files_recursively(const view_t *view, const char path[],
 		{
 			if(!is_symlink(full_path))
 			{
-				list_files_recursively(view, full_path, skip_dot_files, list);
+				list_files_recursively(view, full_path, skip_dot_files, flags, list);
 			}
 			free(full_path);
 			update_string(&lst[i], NULL);
@@ -690,27 +902,54 @@ list_files_recursively(const view_t *view, const char path[],
 }
 
 /* Computes fingerprint of the file specified by path and entry.  Type of the
- * fingerprint is determined by ct parameter.  Returns newly allocated string
- * with the fingerprint, which is empty or NULL on error. */
+ * fingerprint is determined by ct parameter.  Lazy fingerprint is an
+ * optimization which prevents computing contents fingerprint until there is
+ * more than one file of the given size.  Returns newly allocated string with
+ * the fingerprint, which is empty or NULL on error. */
 static char *
 get_file_fingerprint(const char path[], const dir_entry_t *entry,
-		CompareType ct)
+		CompareType ct, int flags, int lazy)
 {
 	switch(ct)
 	{
+		int case_sensitive;
 		char name[NAME_MAX + 1];
 
 		case CT_NAME:
-			if(case_sensitive_paths(path))
+			if(flags & CF_IGNORE_CASE)
+			{
+				case_sensitive = 0;
+			}
+			else if(flags & CF_RESPECT_CASE)
+			{
+				case_sensitive = 1;
+			}
+			else
+			{
+				case_sensitive = case_sensitive_paths(path);
+			}
+
+			if(case_sensitive)
 			{
 				return strdup(entry->name);
 			}
+
 			str_to_lower(entry->name, name, sizeof(name));
 			return strdup(name);
 		case CT_SIZE:
 			return format_str("%" PRINTF_ULL, (unsigned long long)entry->size);
 		case CT_CONTENTS:
-			return get_contents_fingerprint(path, entry);
+			if(lazy)
+			{
+				/* Comparing by contents can't be done if file can't be read. */
+				if(os_access(path, R_OK) != 0)
+				{
+					return strdup("");
+				}
+
+				return format_str("%" PRINTF_ULL, (unsigned long long)entry->size);
+			}
+			return get_contents_fingerprint(path, entry->size);
 	}
 	assert(0 && "Unexpected diffing type.");
 	return strdup("");
@@ -719,7 +958,7 @@ get_file_fingerprint(const char path[], const dir_entry_t *entry,
 /* Makes fingerprint of file contents (all or part of it of fixed size).
  * Returns the fingerprint as a string, which is empty or NULL on error. */
 static char *
-get_contents_fingerprint(const char path[], const dir_entry_t *entry)
+get_contents_fingerprint(const char path[], unsigned long long size)
 {
 	char block[BLOCK_SIZE];
 	size_t to_read = PREFIX_SIZE;
@@ -760,46 +999,104 @@ get_contents_fingerprint(const char path[], const dir_entry_t *entry)
 	const unsigned long long digest = XXH3_64bits_digest(st);
 	XXH3_freeState(st);
 
-	return format_str("%" PRINTF_ULL "|%" PRINTF_ULL,
-			(unsigned long long)entry->size, digest);
+	return format_str("%" PRINTF_ULL "|%" PRINTF_ULL, size, digest);
 }
 
-/* Retrieves file from the trie by its fingerprint.  Returns non-zero if it was
- * in the trie and sets *id, otherwise zero is returned. */
+/* Looks up file in the trie by its fingerprint.  Returns id for the file or -1
+ * if it should be skipped. */
 static int
-get_file_id(trie_t *trie, const char path[], const char fingerprint[], int *id,
-		CompareType ct)
+add_file_to_diff(trie_t *trie, const char path[], dir_entry_t *entry,
+		CompareType ct, int dups_only, int flags, int *next_id)
 {
-	void *data;
-	compare_record_t *record;
-	if(trie_get(trie, fingerprint, &data) != 0)
+	char *fingerprint = get_file_fingerprint(path, entry, ct, flags, /*lazy=*/1);
+	if(is_null_or_empty(fingerprint))
 	{
-		return 0;
+		/* In case we couldn't obtain fingerprint (e.g., comparing by contents and
+		 * the file isn't readable), ignore the file and keep going. */
+		free(fingerprint);
+		return -1;
 	}
-	record = data;
 
-	/* Comparison by contents is the only one when we need to resolve fingerprint
-	 * conflicts. */
-	if(ct != CT_CONTENTS)
-	{
-		*id = record->id;
-		return 1;
-	}
+	void *data = NULL;
+	(void)trie_get(trie, fingerprint, &data);
 
-	/* Fingerprint does not guarantee a match, go through files and find file with
-	 * identical content. */
-	do
+	compare_record_t *record = data;
+	int is_partial = (ct == CT_CONTENTS);
+
+	/* Comparison by contents is the only one when we need to account for lazy
+	 * fingerprint computation or resolve fingerprint conflicts. */
+	if(record != NULL && ct == CT_CONTENTS)
 	{
-		if(files_are_identical(path, record->path))
+		free(fingerprint);
+		is_partial = 0;
+
+		fingerprint = get_file_fingerprint(path, entry, ct, flags, /*lazy=*/0);
+		if(is_null_or_empty(fingerprint))
 		{
-			*id = record->id;
-			return 1;
+			/* In case we couldn't obtain fingerprint (e.g., comparing by contents and
+			 * the file isn't readable), ignore the file and keep going. */
+			free(fingerprint);
+			return -1;
 		}
-		record = record->next;
+
+		if(record->is_partial)
+		{
+			/* There is another file of the same size whose contents fingerprint
+			 * hasn't been computed yet.  Do it here. */
+			char *other_fingerprint = get_contents_fingerprint(record->path,
+					entry->size);
+			if(is_null_or_empty(fingerprint))
+			{
+				/* That other file has issues, don't update it and skip any other file
+				 * that can conflict with it by size.  The file itself won't be skipped
+				 * though, should it be? */
+				free(other_fingerprint);
+				free(fingerprint);
+				return -1;
+			}
+
+			put_file_id(trie, record->path, other_fingerprint, record->id,
+					/*is_partial=*/0, ct);
+			free(other_fingerprint);
+
+			record->is_partial = 0;
+		}
+
+		/* Repeat trie lookup with contents fingerprint. */
+		(void)trie_get(trie, fingerprint, &data);
+		record = data;
+
+		/* Fingerprint does not guarantee a match, go through files and find file
+		 * with identical contents. */
+		do
+		{
+			if(files_are_identical(path, record->path))
+			{
+				break;
+			}
+			record = record->next;
+		}
+		while(record != NULL);
 	}
-	while(record != NULL);
 
-	return 0;
+	if(record != NULL)
+	{
+		free(fingerprint);
+		return record->id;
+	}
+
+	if(dups_only)
+	{
+		free(fingerprint);
+		return -1;
+	}
+
+	int id = *next_id;
+	++*next_id;
+	put_file_id(trie, path, fingerprint, id, is_partial, ct);
+
+	free(fingerprint);
+	return id;
 }
 
 /* Checks whether two files specified by their names hold identical content.
@@ -851,19 +1148,29 @@ files_are_identical(const char a[], const char b[])
 /* Stores id of a file with given fingerprint in the trie. */
 static void
 put_file_id(trie_t *trie, const char path[], const char fingerprint[], int id,
-		CompareType ct)
+		int is_partial, CompareType ct)
 {
 	compare_record_t *const record = malloc(sizeof(*record));
-	void *data = NULL;
-	(void)trie_get(trie, fingerprint, &data);
 
 	record->id = id;
-	record->next = data;
+	record->is_partial = is_partial;
+	record->next = NULL;
 
 	/* Comparison by contents is the only one when we need to resolve fingerprint
 	 * conflicts. */
 	record->path = (ct == CT_CONTENTS ? strdup(path) : NULL);
 
+	/* Just add new entry to the list if something is already there. */
+	void *data = NULL;
+	(void)trie_get(trie, fingerprint, &data);
+	compare_record_t *prev = data;
+	if(prev != NULL)
+	{
+		prev->next = record;
+		return;
+	}
+
+	/* Otherwise we're the head of the list. */
 	if(trie_set(trie, fingerprint, record) < 0)
 	{
 		free(record->path);
@@ -889,64 +1196,88 @@ free_compare_records(void *ptr)
 int
 compare_move(view_t *from, view_t *to)
 {
-	char from_path[PATH_MAX + 1], to_path[PATH_MAX + 1];
-	char *from_fingerprint, *to_fingerprint;
-
-	const CompareType ct = from->custom.diff_cmp_type;
-
-	dir_entry_t *const curr = &from->dir_entry[from->list_pos];
-	dir_entry_t *const other = &to->dir_entry[from->list_pos];
-
-	if(from->custom.type != CV_DIFF || !from->custom.diff_path_group)
+	const CompareType flags = from->custom.diff_cmp_flags;
+	if(from->custom.type != CV_DIFF || !(flags & CF_GROUP_PATHS))
 	{
 		ui_sb_err("Not in diff mode with path grouping");
 		return 1;
 	}
 
-	if(curr->id == other->id && !fentry_is_fake(curr) && !fentry_is_fake(other))
+	char *from_dir = strdup(replace_home_part(flist_get_dir(from)));
+	char *to_dir = strdup(replace_home_part(flist_get_dir(to)));
+	if(from_dir == NULL || to_dir == NULL)
 	{
-		/* Nothing to do if files are already equal. */
-		return 0;
+		free(from_dir);
+		free(to_dir);
+		return 1;
+	}
+
+	char undo_msg[2*PATH_MAX + 32];
+	snprintf(undo_msg, sizeof(undo_msg), "Diff apply %s -> %s", from_dir, to_dir);
+
+	ops_t *ops = fops_get_ops(OP_COPY, "Applying", from_dir, to_dir);
+	free(from_dir);
+	free(to_dir);
+
+	if(ops == NULL)
+	{
+		show_error_msg("Comparison", "Failed to initialize apply operation");
+		return 1;
 	}
 
-	/* We're going at least to try to update one of views (which might refer to
-	 * the same directory), so schedule a reload. */
+	/* We're going at least to try to update one of the views (which might refer
+	 * to the same directory), so schedule a reload. */
 	ui_view_schedule_reload(from);
 	ui_view_schedule_reload(to);
 
-	if(fentry_is_fake(curr))
+	un_group_open(undo_msg);
+
+	dir_entry_t *entry = NULL;
+	while(iter_selection_or_current_any(curr_view, &entry) && fops_active(ops))
 	{
-		/* Just remove the other file (it can't be fake entry too). */
-		return fops_delete_current(to, 1, 0);
+		compare_move_entry(ops, from, to, entry_to_pos(curr_view, entry));
 	}
 
-	get_full_path_of(curr, sizeof(from_path), from_path);
-	get_full_path_of(other, sizeof(to_path), to_path);
+	un_group_close();
 
-	if(fentry_is_fake(other))
-	{
-		char to_path[PATH_MAX + 1];
-		char canonical[PATH_MAX + 1];
-		snprintf(to_path, sizeof(to_path), "%s/%s/%s", flist_get_dir(to),
-				curr->origin + strlen(flist_get_dir(from)), curr->name);
-		canonicalize_path(to_path, canonical, sizeof(canonical));
+	fops_free_ops(ops);
+	flist_sel_stash(curr_view);
+	return 0;
+}
+
+/* Moves a file identified by an entry from one view to the other. */
+static void
+compare_move_entry(ops_t *ops, view_t *from, view_t *to, int idx)
+{
+	char from_path[PATH_MAX + 1], to_path[PATH_MAX + 1];
+	char *from_fingerprint, *to_fingerprint;
+
+	const CompareType ct = from->custom.diff_cmp_type;
+	const CompareType flags = from->custom.diff_cmp_flags;
 
-		/* Copy current file to position of the other one using relative path with
-		 * different base. */
-		fops_replace(from, canonical, 0);
+	dir_entry_t *const curr = &from->dir_entry[idx];
+	dir_entry_t *const other = &to->dir_entry[idx];
 
-		/* Update the other entry to not be fake. */
-		remove_last_path_component(canonical);
-		replace_string(&other->name, curr->name);
-		replace_string(&other->origin, canonical);
+	if(curr->id == other->id && !fentry_is_fake(curr) && !fentry_is_fake(other))
+	{
+		/* Nothing to do if files are already equal. */
+		return;
 	}
-	else
+
+	if(fentry_is_fake(curr))
 	{
-		/* Overwrite file in the other pane with corresponding file from current
-		 * pane. */
-		fops_replace(from, to_path, 1);
+		/* Just remove the other file (it can't be fake entry too). */
+		fops_delete_entry(ops, to, other, /*use_trash=*/1, /*nested=*/0);
+		return;
 	}
 
+	get_full_path_of(curr, sizeof(from_path), from_path);
+	get_full_path_of(other, sizeof(to_path), to_path);
+
+	/* Overwrite file in the other pane with corresponding file from current
+	 * pane. */
+	fops_replace_entry(ops, from, curr, to, other);
+
 	/* Obtaining file fingerprint relies on size field of entries, so try to load
 	 * it and ignore if it fails. */
 	other->size = get_file_size(to_path);
@@ -954,8 +1285,9 @@ compare_move(view_t *from, view_t *to)
 	/* Try to update id of the other entry by computing fingerprint of both files
 	 * and checking if they match. */
 
-	from_fingerprint = get_file_fingerprint(from_path, curr, ct);
-	to_fingerprint = get_file_fingerprint(to_path, other, ct);
+	from_fingerprint = get_file_fingerprint(from_path, curr, ct, flags,
+			/*lazy=*/0);
+	to_fingerprint = get_file_fingerprint(to_path, other, ct, flags, /*lazy=*/0);
 
 	if(!is_null_or_empty(from_fingerprint) && !is_null_or_empty(to_fingerprint))
 	{
@@ -972,8 +1304,6 @@ compare_move(view_t *from, view_t *to)
 
 	free(from_fingerprint);
 	free(to_fingerprint);
-
-	return 0;
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/src/compare.h b/src/compare.h
index 903ba2a..0bf50f6 100644
--- a/src/compare.h
+++ b/src/compare.h
@@ -21,24 +21,39 @@
 
 #include "ui/ui.h"
 
-/* Type of files to list after a comparison. */
+/* Comparison flags. */
 typedef enum
 {
-	LT_ALL,    /* All files. */
-	LT_DUPS,   /* Files that have at least 1 dup on other side or in this view. */
-	LT_UNIQUE, /* Files unique to this view or within this view. */
+	CF_NONE              = 0, /* No flags. */
+	CF_GROUP_PATHS       = 1, /* Otherwise ids are grouped.  Only for two-pane
+                             * compare. */
+	CF_SKIP_EMPTY        = 2, /* Exclude empty files from comparison. */
+	CF_IGNORE_CASE       = 4, /* Compare file names case-insensitively. */
+	CF_RESPECT_CASE      = 8, /* Compare file names case-sensitively.  Case is
+	                             OS/FS-specific if neither CF_*_CASE flag is set. */
+
+	CF_SHOW_IDENTICAL    = 16,  /* Show identical files in comparison. */
+	CF_SHOW_DIFFERENT    = 32,  /* Show different files in comparison. */
+	CF_SHOW_UNIQUE_LEFT  = 64,  /* Show unique left files in comparison. */
+	CF_SHOW_UNIQUE_RIGHT = 128, /* Show unique right files in comparison. */
+
+	CF_SINGLE_PANE       = 256, /* Single pane mode */
+
+	/* Mask of show* flags. */
+	CF_SHOW = CF_SHOW_IDENTICAL
+	        | CF_SHOW_DIFFERENT
+	        | CF_SHOW_UNIQUE_LEFT
+	        | CF_SHOW_UNIQUE_RIGHT,
 }
-ListType;
+CompareFlags;
 
-/* Composes two panes containing information about derived from two file system
- * trees.  If group_paths is zero, views are sorted by ids.  Returns non-zero if
- * status bar message should be preserved. */
-int compare_two_panes(CompareType ct, ListType lt, int group_paths,
-		int skip_empty);
+/* Composes two panes containing information about files derived from two file
+ * system trees.  Returns non-zero if status bar message should be preserved. */
+int compare_two_panes(CompareType ct, ListType lt, int flags);
 
 /* Replaces single pane with information derived from its files.  Returns
  * non-zero if status bar message should be preserved. */
-int compare_one_pane(view_t *view, CompareType ct, ListType lt, int skip_empty);
+int compare_one_pane(view_t *view, CompareType ct, ListType lt, int flags);
 
 /* Moves current file from one view to the other.  Returns non-zero if status
  * bar message should be preserved. */
diff --git a/src/compat/curses.h b/src/compat/curses.h
index 463d633..9d8c782 100644
--- a/src/compat/curses.h
+++ b/src/compat/curses.h
@@ -31,6 +31,24 @@
  * for implementation as it needs more than just wchar_t.) */
 #define K(x) ((wchar_t)((wint_t)0xe000 + 1 + (x)))
 
+/* getmouse() is ncurses-specific, use it in the source and handle naming
+ * discrepancy with PDCurses. */
+#ifdef __PDCURSES__
+#define getmouse nc_getmouse
+#endif
+
+/* In 32-bit mode curses doesn't have enough space for a 5th button, use 2nd
+ * button instead. */
+#ifndef BUTTON5_PRESSED
+
+#define	BUTTON5_RELEASED        BUTTON2_RELEASED
+#define	BUTTON5_PRESSED         BUTTON2_PRESSED
+#define	BUTTON5_CLICKED         BUTTON2_CLICKED
+#define	BUTTON5_DOUBLE_CLICKED  BUTTON2_DOUBLE_CLICKED
+#define	BUTTON5_TRIPLE_CLICKED  BUTTON2_TRIPLE_CLICKED
+
+#endif
+
 /* OpenBSD used to have perverted ncursesw library.  It had stubs with infinite
  * loops instead of real wide functions.  As there is only a couple of wide
  * functions in use, they can be emulated on systems like that. */
diff --git a/src/compat/os.c b/src/compat/os.c
index 0f81a1f..8ef97d5 100644
--- a/src/compat/os.c
+++ b/src/compat/os.c
@@ -366,5 +366,19 @@ os_fdatasync(int fd)
 
 #endif
 
+#include <stdio.h> /* FILE */
+
+#include "../utils/fs.h"
+#include "../utils/path.h"
+#include "fs_limits.h"
+
+FILE *
+os_tmpfile(void)
+{
+	char tmp_path[PATH_MAX + 1];
+	return make_file_in_tmp("vifm.tmp", 0600, /*auto_delete=*/1, tmp_path,
+			sizeof(tmp_path));
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 filetype=c : */
diff --git a/src/compat/os.h b/src/compat/os.h
index 57f1bc3..0d81bc5 100644
--- a/src/compat/os.h
+++ b/src/compat/os.h
@@ -49,7 +49,6 @@
 #define os_rmdir rmdir
 #define os_stat stat
 #define os_system system
-#define os_tmpfile tmpfile
 #define os_getcwd getcwd
 
 int os_fdatasync(int fd);
@@ -62,10 +61,6 @@ int os_fdatasync(int fd);
 /* Not straight forward for Windows and not very important. */
 #define os_lstat os_stat
 
-/* Windows has tmpfile(), but (prepare yourself) it requires administrative
- * privileges... */
-#define os_tmpfile win_tmpfile
-
 struct stat;
 
 int os_access(const char pathname[], int mode);
@@ -103,6 +98,18 @@ char * os_getcwd(char buf[], size_t size);
 
 #endif
 
+/* *nix systems generate predictable names and can open an already existing
+ * file, which can be abused to trick the application into opening an existing
+ * file and bypass security measures.  mkstemp() isn't very helpful because of
+ * umask() which doesn't play nice with threads...
+ *
+ * Windows has tmpfile(), but (prepare yourself) it requires administrative
+ * privileges on modern versions...
+ *
+ * Hence reimplementation which should be reasonably safe and uniform across
+ * supported platforms. */
+FILE * os_tmpfile(void);
+
 #endif /* VIFM__COMPAT__OS_H__ */
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/src/engine/keys.c b/src/engine/keys.c
index a631d05..6210558 100644
--- a/src/engine/keys.c
+++ b/src/engine/keys.c
@@ -145,6 +145,7 @@ static void remove_chunk(key_chunk_t *chunk);
 static int add_list_of_keys(key_chunk_t *root, keys_add_info_t cmds[],
 		size_t len);
 static key_chunk_t * add_keys_inner(key_chunk_t *root, const wchar_t *keys);
+static void init_chunk_data(key_chunk_t *chunk, wchar_t key, KeyType type);
 static void list_chunk(const key_chunk_t *chunk, const wchar_t lhs[],
 		void *arg);
 static void inc_counter(const keys_info_t *keys_info, size_t by);
@@ -1010,11 +1011,6 @@ vle_keys_foreign_add(const wchar_t lhs[], const key_conf_t *info,
 		return -1;
 	}
 
-	if(curr->type == USER_CMD)
-	{
-		free(curr->conf.data.cmd);
-	}
-
 	curr->type = (info->followed == FOLLOWED_BY_NONE) ? BUILTIN_KEYS
 	                                                  : BUILTIN_WAIT_POINT;
 	curr->foreign = 1;
@@ -1032,11 +1028,6 @@ vle_keys_user_add(const wchar_t lhs[], const wchar_t rhs[], int mode,
 		return -1;
 	}
 
-	if(curr->type == USER_CMD)
-	{
-		free(curr->conf.data.cmd);
-	}
-
 	curr->type = USER_CMD;
 	curr->conf.data.cmd = vifm_wcsdup(rhs);
 	curr->no_remap = ((flags & KEYS_FLAG_NOREMAP) != 0);
@@ -1187,16 +1178,13 @@ add_keys_inner(key_chunk_t *root, const wchar_t *keys)
 		{
 			key_chunk_t *c = malloc(sizeof(*c));
 			if(c == NULL)
+			{
 				return NULL;
-			c->key = *keys;
-			c->type = (keys[1] == L'\0') ? BUILTIN_KEYS : BUILTIN_WAIT_POINT;
-			c->conf.data.handler = NULL;
-			c->conf.data.cmd = NULL;
-			c->conf.followed = FOLLOWED_BY_NONE;
-			c->conf.suggest = NULL;
-			c->conf.descr = NULL;
-			c->conf.nim = 0;
-			c->conf.skip_suggestion = 0;
+			}
+
+			KeyType type = (keys[1] == L'\0' ? BUILTIN_KEYS : BUILTIN_WAIT_POINT);
+			init_chunk_data(c, *keys, type);
+
 			c->prev = prev;
 			c->next = p;
 			c->child = NULL;
@@ -1204,10 +1192,6 @@ add_keys_inner(key_chunk_t *root, const wchar_t *keys)
 			c->children_count = 0;
 			c->enters = 0;
 			c->deleted = 0;
-			c->foreign = 0;
-			c->no_remap = 1;
-			c->silent = 0;
-			c->wait = 0;
 			if(prev == NULL)
 				curr->child = c;
 			else
@@ -1229,9 +1213,39 @@ add_keys_inner(key_chunk_t *root, const wchar_t *keys)
 		keys++;
 		curr = p;
 	}
+
+	/* Reset most of the fields of a previously existing key before returning
+	 * it. */
+	if(curr->type == USER_CMD)
+	{
+		free(curr->conf.data.cmd);
+	}
+	init_chunk_data(curr, curr->key, BUILTIN_KEYS);
+
 	return curr;
 }
 
+/* Initializes key metadata and its configuration fields, but not lifetime/tree
+ * structure management parts of the structure. */
+static void
+init_chunk_data(key_chunk_t *chunk, wchar_t key, KeyType type)
+{
+	chunk->key = key;
+	chunk->type = type;
+	chunk->foreign = 0;
+	chunk->no_remap = 1;
+	chunk->silent = 0;
+	chunk->wait = 0;
+
+	chunk->conf.data.cmd = NULL;
+	chunk->conf.followed = FOLLOWED_BY_NONE;
+	chunk->conf.nim = 0;
+	chunk->conf.skip_suggestion = 0;
+	chunk->conf.suggest = NULL;
+	chunk->conf.descr = NULL;
+	chunk->conf.user_data = NULL;
+}
+
 void
 vle_keys_list(int mode, vle_keys_list_cb cb, int user_only)
 {
@@ -1259,7 +1273,8 @@ vle_keys_list(int mode, vle_keys_list_cb cb, int user_only)
 static void
 list_chunk(const key_chunk_t *chunk, const wchar_t lhs[], void *arg)
 {
-	if(chunk->children_count == 0 || chunk->type == USER_CMD)
+	if(chunk->children_count == 0 || chunk->type == USER_CMD ||
+			chunk->conf.followed == FOLLOWED_BY_SELECTOR)
 	{
 		const wchar_t *rhs = (chunk->type == USER_CMD) ? chunk->conf.data.cmd : L"";
 		const char *descr = (chunk->conf.descr == NULL) ? "" : chunk->conf.descr;
@@ -1281,9 +1296,9 @@ inc_counter(const keys_info_t *keys_info, size_t by)
 {
 	assert(enters_counter > 0);
 
-	if(!is_recursive())
+	if(!is_recursive() && !keys_info->mapped)
 	{
-		counter += keys_info->mapped ? 0 : by;
+		counter += by;
 	}
 }
 
diff --git a/src/engine/parsing.c b/src/engine/parsing.c
index 40800ea..8b97d2c 100644
--- a/src/engine/parsing.c
+++ b/src/engine/parsing.c
@@ -16,7 +16,8 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
  */
 
-/* The parsing and evaluation are mostly separated.  Currently parsing evaluates
+/*
+ * The parsing and evaluation are mostly separated.  Currently parsing evaluates
  * values without side-effects, but this can be changed later.
  *
  * Output of parsing phase is an expression tree, which is made of nodes of type
@@ -33,12 +34,13 @@
  * Second type is for the rest of builtins and user-provided functions.
  *
  * parse_or_expr() is a root-level parser of expressions and it basically
- * performs parsing phase.  parse_or_expr() evaluates expression, which is the
+ * performs parsing phase.  eval_expr() evaluates expression, which is the
  * second phase.
  *
- * If parsing stops before the end of an expression, partial result is stored in
- * global variables to be queried by client code (this way expressions can
- * follow one another on a line and parsed sequentially). */
+ * If parsing stops before the end of an expression, partial result is still
+ * returned as a result for the API client (this way expressions can follow one
+ * another on a line and be parsed sequentially).
+ */
 
 #include "parsing.h"
 
@@ -103,12 +105,27 @@ typedef enum
 }
 Ops;
 
-/* Evaluation context that's passed to all eval_*() functions. */
+/* Information about a single token. */
+typedef struct
+{
+	TOKENS_TYPE type; /* Type of the token. */
+	char c;           /* Last character of the token. */
+	char str[3];      /* Full token string. */
+}
+parse_token_t;
+
+/* Context that's passed to almost all functions. */
 typedef struct
 {
-	const int interactive; /* Whether call is being executed by the user. */
+	int interactive; /* Whether parsing is being executed by the user. */
+
+	ParsingErrors last_error; /* Last error (whether in parsing or evaluation). */
+
+	parse_token_t last_token;  /* Current token. */
+	parse_token_t prev_token;  /* Previous token. */
+	const char *last_position; /* Last position in the input. */
 }
-eval_context_t;
+parse_context_t;
 
 /* Defines expression and how to evaluate its value.  Value is stored here after
  * evaluation. */
@@ -131,56 +148,46 @@ typedef struct
 }
 sbuffer;
 
-static int eval_expr(eval_context_t *ctx, expr_t *expr);
-static int eval_or_op(eval_context_t *ctx, int nops, expr_t ops[],
+static int eval_expr(parse_context_t *ctx, expr_t *expr);
+static int eval_or_op(parse_context_t *ctx, int nops, expr_t ops[],
 		var_t *result);
-static int eval_and_op(eval_context_t *ctx, int nops, expr_t ops[],
+static int eval_and_op(parse_context_t *ctx, int nops, expr_t ops[],
 		var_t *result);
-static int eval_call_op(eval_context_t *ctx, const char name[], int nops,
+static int eval_call_op(parse_context_t *ctx, const char name[], int nops,
 		expr_t ops[], var_t *result);
 static int compare_variables(TOKENS_TYPE operation, var_t lhs, var_t rhs);
-static var_t eval_concat(eval_context_t *ctx, int nops, expr_t ops[]);
+static var_t eval_concat(parse_context_t *ctx, int nops, expr_t ops[]);
 static int add_expr_op(expr_t *expr, const expr_t *arg);
 static void free_expr(const expr_t *expr);
-static expr_t parse_or_expr(const char **in);
-static expr_t parse_and_expr(const char **in);
-static expr_t parse_comp_expr(const char **in);
+static expr_t parse_or_expr(parse_context_t *ctx, const char **in);
+static expr_t parse_and_expr(parse_context_t *ctx, const char **in);
+static expr_t parse_comp_expr(parse_context_t *ctx, const char **in);
 static int is_comparison_operator(TOKENS_TYPE type);
-static expr_t parse_factor(const char **in);
-static expr_t parse_concat_expr(const char **in);
-static expr_t parse_term(const char **in);
-static expr_t parse_signed_number(const char **in);
-static var_t parse_number(const char **in);
-static var_t parse_singly_quoted_string(const char **in);
-static int parse_singly_quoted_char(const char **in, sbuffer *sbuf);
-static var_t parse_doubly_quoted_string(const char **in);
-static int parse_doubly_quoted_char(const char **in, sbuffer *sbuf);
-static var_t eval_envvar(const char **in);
-static var_t eval_builtinvar(const char **in);
-static var_t eval_opt(const char **in);
-static expr_t parse_logical_not(const char **in);
-static int parse_sequence(const char **in, const char first[],
-		const char other[], size_t buf_len, char buf[]);
-static expr_t parse_funccall(const char **in);
-static void parse_arglist(const char **in, expr_t *call_expr);
-static void skip_whitespace_tokens(const char **in);
-static void get_next(const char **in);
-
-/* This contains information about the last tokens read. */
-static struct
-{
-	TOKENS_TYPE type; /* Type of the token. */
-	char c;           /* Last character of the token. */
-	char str[3];      /* Full token string. */
-}
-prev_token, last_token;
+static expr_t parse_factor(parse_context_t *ctx, const char **in);
+static expr_t parse_concat_expr(parse_context_t *ctx, const char **in);
+static expr_t parse_term(parse_context_t *ctx, const char **in);
+static expr_t parse_signed_number(parse_context_t *ctx, const char **in);
+static var_t parse_number(parse_context_t *ctx, const char **in);
+static var_t parse_singly_quoted_string(parse_context_t *ctx, const char **in);
+static int parse_singly_quoted_char(parse_context_t *ctx, const char **in,
+		sbuffer *sbuf);
+static var_t parse_doubly_quoted_string(parse_context_t *ctx, const char **in);
+static int parse_doubly_quoted_char(parse_context_t *ctx, const char **in,
+		sbuffer *sbuf);
+static var_t eval_envvar(parse_context_t *ctx, const char **in);
+static var_t eval_builtinvar(parse_context_t *ctx, const char **in);
+static var_t eval_opt(parse_context_t *ctx, const char **in);
+static expr_t parse_logical_not(parse_context_t *ctx, const char **in);
+static int parse_sequence(parse_context_t *ctx, const char **in,
+		const char first[], const char other[], size_t buf_len, char buf[]);
+static expr_t parse_funccall(parse_context_t *ctx, const char **in);
+static void parse_arglist(parse_context_t *ctx, const char **in,
+		expr_t *call_expr);
+static void skip_whitespace_tokens(parse_context_t *ctx, const char **in);
+static void get_next(parse_context_t *ctx, const char **in);
 
 static int initialized;
 static getenv_func getenv_fu;
-static ParsingErrors last_error;
-static const char *last_position;
-static const char *last_parsed_char;
-static var_t res_val;
 
 /* Empty expression to be returned on errors. */
 static expr_t null_expr;
@@ -188,97 +195,74 @@ static expr_t null_expr;
 /* Public interface --------------------------------------------------------- */
 
 void
-init_parser(getenv_func getenv_f)
+vle_parser_init(getenv_func getenv_f)
 {
 	getenv_fu = getenv_f;
 	initialized = 1;
 }
 
-const char *
-get_last_position(void)
+parsing_result_t
+vle_parser_eval(const char input[], int interactive)
 {
 	assert(initialized && "Parser must be initialized before use.");
-	return last_position;
-}
 
-const char *
-get_last_parsed_char(void)
-{
-	assert(initialized && "Parser must be initialized before use.");
-	return last_parsed_char;
-}
-
-ParsingErrors
-parse(const char input[], int interactive, var_t *result)
-{
-	expr_t expr_root;
-
-	assert(initialized && "Parser must be initialized before use.");
+	parsing_result_t result = {};
 
-	last_error = PE_NO_ERROR;
-	last_token.type = BEGIN;
+	parse_context_t ctx = {
+		.interactive = interactive,
 
-	last_position = input;
-	get_next(&last_position);
-	expr_root = parse_or_expr(&last_position);
-	last_parsed_char = last_position;
+		.last_error = PE_NO_ERROR,
+		.last_token.type = BEGIN,
+		.last_position = input,
+	};
 
-	var_free(res_val);
-	res_val = var_error();
+	get_next(&ctx, &ctx.last_position);
+	expr_t expr_root = parse_or_expr(&ctx, &ctx.last_position);
+	result.last_parsed_char = ctx.last_position;
 
-	eval_context_t ctx = { .interactive = interactive };
+	result.value = var_error();
 
-	if(last_token.type != END)
+	if(ctx.last_token.type != END)
 	{
-		if(last_parsed_char > input)
+		if(result.last_parsed_char > input)
 		{
-			last_parsed_char--;
+			--result.last_parsed_char;
 		}
-		if(last_error == PE_NO_ERROR)
+		if(ctx.last_error == PE_NO_ERROR)
 		{
-			if(last_token.type == DQ && strchr(last_position, '"') == NULL)
+			if(ctx.last_token.type == DQ && strchr(ctx.last_position, '"') == NULL)
 			{
 				/* This is a comment, just ignore it. */
-				last_position += strlen(last_position);
+				ctx.last_position += strlen(ctx.last_position);
 			}
 			else if(eval_expr(&ctx, &expr_root) == 0)
 			{
-				res_val = var_clone(expr_root.value);
-				last_error = PE_INVALID_EXPRESSION;
+				result.value = var_clone(expr_root.value);
+				ctx.last_error = PE_INVALID_EXPRESSION;
 			}
 		}
 	}
 
-	if(last_error == PE_NO_ERROR)
+	if(ctx.last_error == PE_NO_ERROR)
 	{
 		if(eval_expr(&ctx, &expr_root) == 0)
 		{
-			res_val = var_clone(expr_root.value);
-			*result = var_clone(expr_root.value);
+			result.value = var_clone(expr_root.value);
 		}
 	}
 
-	if(last_error == PE_INVALID_EXPRESSION)
+	if(ctx.last_error == PE_INVALID_EXPRESSION)
 	{
-		last_position = skip_whitespace(input);
+		ctx.last_position = skip_whitespace(input);
 	}
 
 	free_expr(&expr_root);
-	return last_error;
-}
 
-var_t
-get_parsing_result(void)
-{
-	assert(initialized && "Parser must be initialized before use.");
-	return var_clone(res_val);
-}
+	result.ends_with_whitespace = (ctx.prev_token.type == WHITESPACE);
+	result.last_position = ctx.last_position;
+	result.error = ctx.last_error;
 
-int
-is_prev_token_whitespace(void)
-{
-	assert(initialized && "Parser must be initialized before use.");
-	return prev_token.type == WHITESPACE;
+	return result;
 }
 
 /* Expression evaluation ---------------------------------------------------- */
@@ -286,7 +270,7 @@ is_prev_token_whitespace(void)
 /* Evaluates values of an expression.  Returns zero on success, which means that
  * expr->value is now correct, otherwise non-zero is returned. */
 static int
-eval_expr(eval_context_t *ctx, expr_t *expr)
+eval_expr(parse_context_t *ctx, expr_t *expr)
 {
 	int result = 1;
 	switch(expr->op_type)
@@ -316,7 +300,7 @@ eval_expr(eval_context_t *ctx, expr_t *expr)
 /* Evaluates logical OR operation.  All operands are evaluated lazily from left
  * to right.  Returns zero on success, otherwise non-zero is returned. */
 static int
-eval_or_op(eval_context_t *ctx, int nops, expr_t ops[], var_t *result)
+eval_or_op(parse_context_t *ctx, int nops, expr_t ops[], var_t *result)
 {
 	int val;
 	int i;
@@ -358,7 +342,7 @@ eval_or_op(eval_context_t *ctx, int nops, expr_t ops[], var_t *result)
 /* Evaluates logical AND operation.  All operands are evaluated lazily from left
  * to right.  Returns zero on success, otherwise non-zero is returned. */
 static int
-eval_and_op(eval_context_t *ctx, int nops, expr_t ops[], var_t *result)
+eval_and_op(parse_context_t *ctx, int nops, expr_t ops[], var_t *result)
 {
 	int val;
 	int i;
@@ -400,7 +384,7 @@ eval_and_op(eval_context_t *ctx, int nops, expr_t ops[], var_t *result)
 /* Evaluates invocation operation.  All operands are evaluated beforehand.
  * Returns zero on success, otherwise non-zero is returned. */
 static int
-eval_call_op(eval_context_t *ctx, const char name[], int nops, expr_t ops[],
+eval_call_op(parse_context_t *ctx, const char name[], int nops, expr_t ops[],
 		var_t *result)
 {
 	int i;
@@ -481,14 +465,14 @@ eval_call_op(eval_context_t *ctx, const char name[], int nops, expr_t ops[],
 		*result = function_call(name, &call_info);
 		if(result->type == VTYPE_ERROR)
 		{
-			last_error = PE_INVALID_EXPRESSION;
+			ctx->last_error = PE_INVALID_EXPRESSION;
 			var_free(*result);
 			*result = var_false();
 		}
 		function_call_info_free(&call_info);
 	}
 
-	return (last_error != PE_NO_ERROR);
+	return (ctx->last_error != PE_NO_ERROR);
 }
 
 /* Compares lhs and rhs variables by comparison operator specified by a token.
@@ -537,7 +521,7 @@ compare_variables(TOKENS_TYPE operation, var_t lhs, var_t rhs)
 /* Evaluates concatenation of expressions.  Returns resultant value or variable
  * of type VTYPE_ERROR. */
 static var_t
-eval_concat(eval_context_t *ctx, int nops, expr_t ops[])
+eval_concat(parse_context_t *ctx, int nops, expr_t ops[])
 {
 	char res[CMD_LINE_LENGTH_MAX + 1];
 	size_t res_len = 0U;
@@ -557,7 +541,7 @@ eval_concat(eval_context_t *ctx, int nops, expr_t ops[])
 		char *const str_val = var_to_str(ops[i].value);
 		if(str_val == NULL)
 		{
-			last_error = PE_INTERNAL;
+			ctx->last_error = PE_INTERNAL;
 			break;
 		}
 
@@ -566,7 +550,7 @@ eval_concat(eval_context_t *ctx, int nops, expr_t ops[])
 		free(str_val);
 	}
 
-	return (last_error == PE_NO_ERROR ? var_from_str(res) : var_error());
+	return (ctx->last_error == PE_NO_ERROR ? var_from_str(res) : var_error());
 }
 
 /* Appends operand to an expression.  Returns zero on success, otherwise
@@ -606,14 +590,14 @@ free_expr(const expr_t *expr)
 
 /* or_expr ::= and_expr | and_expr '||' or_expr */
 static expr_t
-parse_or_expr(const char **in)
+parse_or_expr(parse_context_t *ctx, const char **in)
 {
 	expr_t result = { .op_type = OP_OR };
 
-	while(last_error == PE_NO_ERROR)
+	while(ctx->last_error == PE_NO_ERROR)
 	{
-		const expr_t op = parse_and_expr(in);
-		if(last_error != PE_NO_ERROR)
+		const expr_t op = parse_and_expr(ctx, in);
+		if(ctx->last_error != PE_NO_ERROR)
 		{
 			free_expr(&op);
 			break;
@@ -621,20 +605,20 @@ parse_or_expr(const char **in)
 
 		if(add_expr_op(&result, &op) != 0)
 		{
-			last_error = PE_INTERNAL;
+			ctx->last_error = PE_INTERNAL;
 			break;
 		}
 
-		if(last_token.type != OR)
+		if(ctx->last_token.type != OR)
 		{
 			/* Return partial result. */
 			break;
 		}
 
-		get_next(in);
+		get_next(ctx, in);
 	}
 
-	if(last_error == PE_INTERNAL)
+	if(ctx->last_error == PE_INTERNAL)
 	{
 		free_expr(&result);
 		return null_expr;
@@ -645,14 +629,14 @@ parse_or_expr(const char **in)
 
 /* and_expr ::= comp_expr | comp_expr '&&' and_expr */
 static expr_t
-parse_and_expr(const char **in)
+parse_and_expr(parse_context_t *ctx, const char **in)
 {
 	expr_t result = { .op_type = OP_AND };
 
-	while(last_error == PE_NO_ERROR)
+	while(ctx->last_error == PE_NO_ERROR)
 	{
-		const expr_t op = parse_comp_expr(in);
-		if(last_error != PE_NO_ERROR)
+		const expr_t op = parse_comp_expr(ctx, in);
+		if(ctx->last_error != PE_NO_ERROR)
 		{
 			free_expr(&op);
 			break;
@@ -660,20 +644,20 @@ parse_and_expr(const char **in)
 
 		if(add_expr_op(&result, &op) != 0)
 		{
-			last_error = PE_INTERNAL;
+			ctx->last_error = PE_INTERNAL;
 			break;
 		}
 
-		if(last_token.type != AND)
+		if(ctx->last_token.type != AND)
 		{
 			/* Return partial result. */
 			break;
 		}
 
-		get_next(in);
+		get_next(ctx, in);
 	}
 
-	if(last_error == PE_INTERNAL)
+	if(ctx->last_error == PE_INTERNAL)
 	{
 		free_expr(&result);
 		return null_expr;
@@ -685,37 +669,38 @@ parse_and_expr(const char **in)
 /* comp_expr ::= factor | factor op factor
  * op ::= '==' | '!=' | '<' | '<=' | '>' | '>=' */
 static expr_t
-parse_comp_expr(const char **in)
+parse_comp_expr(parse_context_t *ctx, const char **in)
 {
 	expr_t lhs;
 	expr_t rhs;
 	expr_t result = { .op_type = OP_CALL };
 
-	lhs = parse_factor(in);
-	if(last_error != PE_NO_ERROR || !is_comparison_operator(last_token.type))
+	lhs = parse_factor(ctx, in);
+	if(ctx->last_error != PE_NO_ERROR ||
+			!is_comparison_operator(ctx->last_token.type))
 	{
 		return lhs;
 	}
 
-	result.func = strdup(last_token.str);
+	result.func = strdup(ctx->last_token.str);
 
 	if(add_expr_op(&result, &lhs) != 0 || result.func == NULL)
 	{
 		free_expr(&result);
-		last_error = PE_INTERNAL;
+		ctx->last_error = PE_INTERNAL;
 		return null_expr;
 	}
 
-	get_next(in);
-	rhs = parse_factor(in);
+	get_next(ctx, in);
+	rhs = parse_factor(ctx, in);
 	if(add_expr_op(&result, &rhs) != 0)
 	{
 		free_expr(&result);
-		last_error = PE_INTERNAL;
+		ctx->last_error = PE_INTERNAL;
 		return null_expr;
 	}
 
-	if(last_error != PE_NO_ERROR)
+	if(ctx->last_error != PE_NO_ERROR)
 	{
 		free_expr(&result);
 		return null_expr;
@@ -737,27 +722,27 @@ is_comparison_operator(TOKENS_TYPE type)
 /* factor ::= concat_expr { op concat_expr }
  * op ::= '+' | '-' */
 static expr_t
-parse_factor(const char **in)
+parse_factor(parse_context_t *ctx, const char **in)
 {
-	expr_t result = parse_concat_expr(in);
+	expr_t result = parse_concat_expr(ctx, in);
 
-	while(last_error == PE_NO_ERROR &&
-			(last_token.type == PLUS || last_token.type == MINUS))
+	while(ctx->last_error == PE_NO_ERROR &&
+			(ctx->last_token.type == PLUS || ctx->last_token.type == MINUS))
 	{
 		expr_t intermediate = { .op_type = OP_CALL };
 		expr_t next;
 
-		intermediate.func = strdup(last_token.str);
+		intermediate.func = strdup(ctx->last_token.str);
 		if(add_expr_op(&intermediate, &result) != 0 || intermediate.func == NULL)
 		{
-			last_error = PE_INTERNAL;
+			ctx->last_error = PE_INTERNAL;
 			free_expr(&intermediate);
 			return null_expr;
 		}
 
-		get_next(in);
-		next = parse_concat_expr(in);
-		if(last_error != PE_NO_ERROR)
+		get_next(ctx, in);
+		next = parse_concat_expr(ctx, in);
+		if(ctx->last_error != PE_NO_ERROR)
 		{
 			free_expr(&next);
 			free_expr(&intermediate);
@@ -766,7 +751,7 @@ parse_factor(const char **in)
 
 		if(add_expr_op(&intermediate, &next) != 0)
 		{
-			last_error = PE_INTERNAL;
+			ctx->last_error = PE_INTERNAL;
 			free_expr(&intermediate);
 			return null_expr;
 		}
@@ -774,7 +759,7 @@ parse_factor(const char **in)
 		result = intermediate;
 	}
 
-	if(last_error == PE_INTERNAL)
+	if(ctx->last_error == PE_INTERNAL)
 	{
 		free_expr(&result);
 		return null_expr;
@@ -785,25 +770,25 @@ parse_factor(const char **in)
 
 /* concat_expr ::= term { '.' term } */
 static expr_t
-parse_concat_expr(const char **in)
+parse_concat_expr(parse_context_t *ctx, const char **in)
 {
 	expr_t result = { .op_type = OP_CALL };
 
 	result.func = strdup(".");
 	if(result.func == NULL)
 	{
-		last_error = PE_INTERNAL;
+		ctx->last_error = PE_INTERNAL;
 	}
 
-	while(last_error == PE_NO_ERROR)
+	while(ctx->last_error == PE_NO_ERROR)
 	{
 		expr_t op;
 
-		skip_whitespace_tokens(in);
-		op = parse_term(in);
-		skip_whitespace_tokens(in);
+		skip_whitespace_tokens(ctx, in);
+		op = parse_term(ctx, in);
+		skip_whitespace_tokens(ctx, in);
 
-		if(last_error != PE_NO_ERROR)
+		if(ctx->last_error != PE_NO_ERROR)
 		{
 			free_expr(&op);
 			break;
@@ -811,20 +796,20 @@ parse_concat_expr(const char **in)
 
 		if(add_expr_op(&result, &op) != 0)
 		{
-			last_error = PE_INTERNAL;
+			ctx->last_error = PE_INTERNAL;
 			break;
 		}
 
-		if(last_token.type != DOT)
+		if(ctx->last_token.type != DOT)
 		{
 			/* Return partial result. */
 			break;
 		}
 
-		get_next(in);
+		get_next(ctx, in);
 	}
 
-	if(last_error == PE_INTERNAL)
+	if(ctx->last_error == PE_INTERNAL)
 	{
 		free_expr(&result);
 		return null_expr;
@@ -836,67 +821,68 @@ parse_concat_expr(const char **in)
 /* term ::= signed_number | number | sqstr | dqstr | envvar | builtinvar |
  *          funccall | opt | logical_not | '(' or_expr ')' */
 static expr_t
-parse_term(const char **in)
+parse_term(parse_context_t *ctx, const char **in)
 {
 	expr_t result = { .op_type = OP_NONE };
 	const char *old_in = *in - 1;
 
-	switch(last_token.type)
+	switch(ctx->last_token.type)
 	{
 		case MINUS:
 		case PLUS:
-			result = parse_signed_number(in);
+			result = parse_signed_number(ctx, in);
 			break;
 		case DIGIT:
-			result.value = parse_number(in);
+			result.value = parse_number(ctx, in);
 			break;
 		case SQ:
-			get_next(in);
-			result.value = parse_singly_quoted_string(in);
+			get_next(ctx, in);
+			result.value = parse_singly_quoted_string(ctx, in);
 			break;
 		case DQ:
-			get_next(in);
-			result.value = parse_doubly_quoted_string(in);
+			get_next(ctx, in);
+			result.value = parse_doubly_quoted_string(ctx, in);
 			break;
 		case DOLLAR:
-			get_next(in);
-			result.value = eval_envvar(in);
+			get_next(ctx, in);
+			result.value = eval_envvar(ctx, in);
 			break;
 		case AMPERSAND:
-			get_next(in);
-			result.value = eval_opt(in);
+			get_next(ctx, in);
+			result.value = eval_opt(ctx, in);
 			break;
 		case EMARK:
-			get_next(in);
-			result = parse_logical_not(in);
+			get_next(ctx, in);
+			result = parse_logical_not(ctx, in);
 			break;
 		case LPAREN:
-			get_next(in);
-			result = parse_or_expr(in);
-			if(last_token.type == RPAREN)
+			get_next(ctx, in);
+			result = parse_or_expr(ctx, in);
+			if(ctx->last_token.type == RPAREN)
 			{
-				get_next(in);
+				get_next(ctx, in);
 			}
 			else
 			{
 				free_expr(&result);
 				result = null_expr;
 				result.value = var_error();
-				last_error = PE_MISSING_PAREN;
-				last_position = old_in;
+				ctx->last_error = PE_MISSING_PAREN;
+				ctx->last_position = old_in;
 			}
 			break;
 
 		case SYM:
-			if(char_is_one_of("abcdefghijklmnopqrstuvwxyz_", tolower(last_token.c)))
+			if(char_is_one_of("abcdefghijklmnopqrstuvwxyz_",
+						tolower(ctx->last_token.c)))
 			{
 				if(**in == ':')
 				{
-					result.value = eval_builtinvar(in);
+					result.value = eval_builtinvar(ctx, in);
 				}
 				else
 				{
-					result = parse_funccall(in);
+					result = parse_funccall(ctx, in);
 				}
 				break;
 			}
@@ -904,7 +890,7 @@ parse_term(const char **in)
 
 		default:
 			--*in;
-			last_error = PE_INVALID_EXPRESSION;
+			ctx->last_error = PE_INVALID_EXPRESSION;
 			result.value = var_error();
 			break;
 	}
@@ -913,17 +899,17 @@ parse_term(const char **in)
 
 /* signed_number ::= ( + | - ) { + | - } term */
 static expr_t
-parse_signed_number(const char **in)
+parse_signed_number(parse_context_t *ctx, const char **in)
 {
-	const int sign = (last_token.type == MINUS) ? -1 : 1;
+	const int sign = (ctx->last_token.type == MINUS) ? -1 : 1;
 	expr_t result = { .op_type = OP_CALL };
 	expr_t op;
 
-	get_next(in);
-	skip_whitespace_tokens(in);
+	get_next(ctx, in);
+	skip_whitespace_tokens(ctx, in);
 
-	op = parse_term(in);
-	if(last_error != PE_NO_ERROR)
+	op = parse_term(ctx, in);
+	if(ctx->last_error != PE_NO_ERROR)
 	{
 		free_expr(&op);
 		return null_expr;
@@ -933,7 +919,7 @@ parse_signed_number(const char **in)
 	if(add_expr_op(&result, &op) != 0 || result.func == NULL)
 	{
 		free_expr(&result);
-		last_error = PE_INTERNAL;
+		ctx->last_error = PE_INTERNAL;
 		return null_expr;
 	}
 
@@ -942,7 +928,7 @@ parse_signed_number(const char **in)
 
 /* number ::= num { num } */
 static var_t
-parse_number(const char **in)
+parse_number(parse_context_t *ctx, const char **in)
 {
 	char buffer[CMD_LINE_LENGTH_MAX];
 	size_t len = 0U;
@@ -950,103 +936,103 @@ parse_number(const char **in)
 
 	do
 	{
-		if(sstrappendch(buffer, &len, sizeof(buffer), last_token.c) != 0)
+		if(sstrappendch(buffer, &len, sizeof(buffer), ctx->last_token.c) != 0)
 		{
-			last_error = PE_INTERNAL;
+			ctx->last_error = PE_INTERNAL;
 			return var_false();
 		}
-		get_next(in);
+		get_next(ctx, in);
 	}
-	while(last_token.type == DIGIT);
+	while(ctx->last_token.type == DIGIT);
 
 	return var_from_int(str_to_int(buffer));
 }
 
 /* sqstr ::= ''' sqchar { sqchar } ''' */
 static var_t
-parse_singly_quoted_string(const char **in)
+parse_singly_quoted_string(parse_context_t *ctx, const char **in)
 {
 	char buffer[CMD_LINE_LENGTH_MAX + 1];
 	const char *old_in = *in - 2;
 	sbuffer sbuf = { .data = buffer, .size = sizeof(buffer) };
 	buffer[0] = '\0';
-	while(parse_singly_quoted_char(in, &sbuf));
+	while(parse_singly_quoted_char(ctx, in, &sbuf));
 
-	if(last_error != PE_NO_ERROR)
+	if(ctx->last_error != PE_NO_ERROR)
 	{
 		return var_false();
 	}
 
-	if(last_token.type == SQ)
+	if(ctx->last_token.type == SQ)
 	{
-		get_next(in);
+		get_next(ctx, in);
 		return var_from_str(buffer);
 	}
 
-	last_error = PE_MISSING_QUOTE;
-	last_position = old_in;
+	ctx->last_error = PE_MISSING_QUOTE;
+	ctx->last_position = old_in;
 	return var_false();
 }
 
 /* sqchar
  * Returns non-zero if there are more characters in the string. */
 static int
-parse_singly_quoted_char(const char **in, sbuffer *sbuf)
+parse_singly_quoted_char(parse_context_t *ctx, const char **in, sbuffer *sbuf)
 {
 	int double_sq;
 	int sq_char;
 
-	double_sq = (last_token.type == SQ && **in == '\'');
-	sq_char = last_token.type != SQ && last_token.type != END;
+	double_sq = (ctx->last_token.type == SQ && **in == '\'');
+	sq_char = ctx->last_token.type != SQ && ctx->last_token.type != END;
 	if(!sq_char && !double_sq)
 	{
 		return 0;
 	}
 
-	if(sstrappend(sbuf->data, &sbuf->len, sbuf->size, last_token.str) != 0)
+	if(sstrappend(sbuf->data, &sbuf->len, sbuf->size, ctx->last_token.str) != 0)
 	{
-		last_error = PE_INTERNAL;
+		ctx->last_error = PE_INTERNAL;
 		return 0;
 	}
-	get_next(in);
+	get_next(ctx, in);
 
 	if(double_sq)
 	{
-		get_next(in);
+		get_next(ctx, in);
 	}
 	return 1;
 }
 
 /* dqstr ::= ''' dqchar { dqchar } ''' */
 static var_t
-parse_doubly_quoted_string(const char **in)
+parse_doubly_quoted_string(parse_context_t *ctx, const char **in)
 {
 	char buffer[CMD_LINE_LENGTH_MAX + 1];
 	const char *old_in = *in - 2;
 	sbuffer sbuf = { .data = buffer, .size = sizeof(buffer) };
 	buffer[0] = '\0';
-	while(parse_doubly_quoted_char(in, &sbuf));
+	while(parse_doubly_quoted_char(ctx, in, &sbuf));
 
-	if(last_error != PE_NO_ERROR)
+	if(ctx->last_error != PE_NO_ERROR)
 	{
 		return var_false();
 	}
 
-	if(last_token.type == DQ)
+	if(ctx->last_token.type == DQ)
 	{
-		get_next(in);
+		get_next(ctx, in);
 		return var_from_str(buffer);
 	}
 
-	last_error = PE_MISSING_QUOTE;
-	last_position = old_in;
+	ctx->last_error = PE_MISSING_QUOTE;
+	ctx->last_position = old_in;
 	return var_false();
 }
 
 /* dqchar
  * Returns non-zero if there are more characters in the string. */
 int
-parse_doubly_quoted_char(const char **in, sbuffer *sbuf)
+parse_doubly_quoted_char(parse_context_t *ctx, const char **in, sbuffer *sbuf)
 {
 	static const char table[] =
 						/* 00  01  02  03  04  05  06  07  08  09  0a  0b  0c  0d  0e  0f */
@@ -1069,46 +1055,46 @@ parse_doubly_quoted_char(const char **in, sbuffer *sbuf)
 
 	int ok;
 
-	if(last_token.type == DQ || last_token.type == END)
+	if(ctx->last_token.type == DQ || ctx->last_token.type == END)
 	{
 		return 0;
 	}
 
-	if(last_token.c == '\\')
+	if(ctx->last_token.c == '\\')
 	{
-		get_next(in);
-		if(last_token.type == END)
+		get_next(ctx, in);
+		if(ctx->last_token.type == END)
 		{
-			last_error = PE_INVALID_EXPRESSION;
+			ctx->last_error = PE_INVALID_EXPRESSION;
 			return 0;
 		}
 		ok = sstrappendch(sbuf->data, &sbuf->len, sbuf->size,
-				table[(int)last_token.c]);
+				table[(int)ctx->last_token.c]);
 	}
 	else
 	{
-		ok = sstrappend(sbuf->data, &sbuf->len, sbuf->size, last_token.str);
+		ok = sstrappend(sbuf->data, &sbuf->len, sbuf->size, ctx->last_token.str);
 	}
 
 	if(ok != 0)
 	{
-		last_error = PE_INTERNAL;
+		ctx->last_error = PE_INTERNAL;
 		return 0;
 	}
 
-	get_next(in);
+	get_next(ctx, in);
 	return 1;
 }
 
 /* envvar ::= '$' envvarname */
 static var_t
-eval_envvar(const char **in)
+eval_envvar(parse_context_t *ctx, const char **in)
 {
 	char name[VAR_NAME_LENGTH_MAX + 1];
-	if(!parse_sequence(in, ENV_VAR_NAME_FIRST_CHAR, ENV_VAR_NAME_CHARS,
+	if(!parse_sequence(ctx, in, ENV_VAR_NAME_FIRST_CHAR, ENV_VAR_NAME_CHARS,
 		sizeof(name), name))
 	{
-		last_error = PE_INVALID_EXPRESSION;
+		ctx->last_error = PE_INVALID_EXPRESSION;
 		return var_false();
 	}
 
@@ -1117,33 +1103,33 @@ eval_envvar(const char **in)
 
 /* builtinvar ::= 'v:' varname */
 static var_t
-eval_builtinvar(const char **in)
+eval_builtinvar(parse_context_t *ctx, const char **in)
 {
 	var_t var_value;
 	char name[VAR_NAME_LENGTH_MAX + 1];
 	strcpy(name, "v:");
 
-	if(last_token.c != 'v' || **in != ':')
+	if(ctx->last_token.c != 'v' || **in != ':')
 	{
-		last_error = PE_INVALID_EXPRESSION;
+		ctx->last_error = PE_INVALID_EXPRESSION;
 		return var_false();
 	}
 
-	get_next(in);
-	get_next(in);
+	get_next(ctx, in);
+	get_next(ctx, in);
 
 	/* XXX: re-using environment variable constants, but could make new ones. */
-	if(!parse_sequence(in, ENV_VAR_NAME_FIRST_CHAR, ENV_VAR_NAME_CHARS,
+	if(!parse_sequence(ctx, in, ENV_VAR_NAME_FIRST_CHAR, ENV_VAR_NAME_CHARS,
 				sizeof(name) - 2U, &name[2]))
 	{
-		last_error = PE_INVALID_EXPRESSION;
+		ctx->last_error = PE_INVALID_EXPRESSION;
 		return var_false();
 	}
 
 	var_value = getvar(name);
 	if(var_value.type == VTYPE_ERROR)
 	{
-		last_error = PE_INVALID_EXPRESSION;
+		ctx->last_error = PE_INVALID_EXPRESSION;
 		return var_false();
 	}
 
@@ -1152,31 +1138,31 @@ eval_builtinvar(const char **in)
 
 /* envvar ::= '&' [ 'l:' | 'g:' ] optname */
 static var_t
-eval_opt(const char **in)
+eval_opt(parse_context_t *ctx, const char **in)
 {
 	OPT_SCOPE scope = OPT_ANY;
 	const opt_t *option;
 
 	char name[OPTION_NAME_MAX + 1];
 
-	if((last_token.c == 'l' || last_token.c == 'g') && **in == ':')
+	if((ctx->last_token.c == 'l' || ctx->last_token.c == 'g') && **in == ':')
 	{
-		scope = (last_token.c == 'l') ? OPT_LOCAL : OPT_GLOBAL;
-		get_next(in);
-		get_next(in);
+		scope = (ctx->last_token.c == 'l') ? OPT_LOCAL : OPT_GLOBAL;
+		get_next(ctx, in);
+		get_next(ctx, in);
 	}
 
-	if(!parse_sequence(in, OPT_NAME_FIRST_CHAR, OPT_NAME_CHARS, sizeof(name),
+	if(!parse_sequence(ctx, in, OPT_NAME_FIRST_CHAR, OPT_NAME_CHARS, sizeof(name),
 		name))
 	{
-		last_error = PE_INVALID_EXPRESSION;
+		ctx->last_error = PE_INVALID_EXPRESSION;
 		return var_false();
 	}
 
 	option = vle_opts_find(name, scope);
 	if(option == NULL)
 	{
-		last_error = PE_INVALID_EXPRESSION;
+		ctx->last_error = PE_INVALID_EXPRESSION;
 		return var_false();
 	}
 
@@ -1205,15 +1191,15 @@ eval_opt(const char **in)
 
 /* logical_not ::= '!' term */
 static expr_t
-parse_logical_not(const char **in)
+parse_logical_not(parse_context_t *ctx, const char **in)
 {
 	expr_t result = { .op_type = OP_CALL };
 	expr_t op;
 
-	skip_whitespace_tokens(in);
+	skip_whitespace_tokens(ctx, in);
 
-	op = parse_term(in);
-	if(last_error != PE_NO_ERROR)
+	op = parse_term(ctx, in);
+	if(ctx->last_error != PE_NO_ERROR)
 	{
 		free_expr(&op);
 		return null_expr;
@@ -1221,14 +1207,14 @@ parse_logical_not(const char **in)
 
 	if(add_expr_op(&result, &op) != 0)
 	{
-		last_error = PE_INTERNAL;
+		ctx->last_error = PE_INTERNAL;
 		return null_expr;
 	}
 
 	result.func = strdup("!");
 	if(result.func == NULL)
 	{
-		last_error = PE_INTERNAL;
+		ctx->last_error = PE_INTERNAL;
 		free_expr(&result);
 		return null_expr;
 	}
@@ -1239,10 +1225,10 @@ parse_logical_not(const char **in)
 /* sequence ::= first { other }
  * Returns zero on failure, otherwise non-zero is returned. */
 static int
-parse_sequence(const char **in, const char first[], const char other[],
-		size_t buf_len, char buf[])
+parse_sequence(parse_context_t *ctx, const char **in, const char first[],
+		const char other[], size_t buf_len, char buf[])
 {
-	if(buf_len == 0UL || !char_is_one_of(first, last_token.c))
+	if(buf_len == 0UL || !char_is_one_of(first, ctx->last_token.c))
 	{
 		return 0;
 	}
@@ -1251,60 +1237,60 @@ parse_sequence(const char **in, const char first[], const char other[],
 
 	do
 	{
-		strcatch(buf, last_token.c);
-		get_next(in);
+		strcatch(buf, ctx->last_token.c);
+		get_next(ctx, in);
 	}
-	while(--buf_len > 1UL && char_is_one_of(other, last_token.c));
+	while(--buf_len > 1UL && char_is_one_of(other, ctx->last_token.c));
 
 	return 1;
 }
 
 /* funccall ::= varname '(' [arglist] ')' */
 static expr_t
-parse_funccall(const char **in)
+parse_funccall(parse_context_t *ctx, const char **in)
 {
 	char *name;
 	size_t name_len;
 	expr_t result = { .op_type = OP_CALL };
 
-	if(!isalpha(last_token.c))
+	if(!isalpha(ctx->last_token.c))
 	{
-		last_error = PE_INVALID_EXPRESSION;
+		ctx->last_error = PE_INVALID_EXPRESSION;
 		return null_expr;
 	}
 
-	name = strdup(last_token.str);
+	name = strdup(ctx->last_token.str);
 	name_len = strlen(name);
-	get_next(in);
-	while(last_token.type == SYM && isalnum(last_token.c))
+	get_next(ctx, in);
+	while(ctx->last_token.type == SYM && isalnum(ctx->last_token.c))
 	{
-		if(strappendch(&name, &name_len, last_token.c) != 0)
+		if(strappendch(&name, &name_len, ctx->last_token.c) != 0)
 		{
 			free(name);
-			last_error = PE_INTERNAL;
+			ctx->last_error = PE_INTERNAL;
 			return null_expr;
 		}
-		get_next(in);
+		get_next(ctx, in);
 	}
 
-	if(last_token.type != LPAREN || !function_registered(name))
+	if(ctx->last_token.type != LPAREN || !function_registered(name))
 	{
 		free(name);
-		last_error = PE_INVALID_EXPRESSION;
+		ctx->last_error = PE_INVALID_EXPRESSION;
 		return null_expr;
 	}
 
 	result.func = name;
 
-	get_next(in);
-	skip_whitespace_tokens(in);
+	get_next(ctx, in);
+	skip_whitespace_tokens(ctx, in);
 
 	/* If argument list is not empty. */
-	if(last_token.type != RPAREN)
+	if(ctx->last_token.type != RPAREN)
 	{
 		const char *old_in = *in - 1;
-		parse_arglist(in, &result);
-		if(last_error != PE_NO_ERROR)
+		parse_arglist(ctx, in, &result);
+		if(ctx->last_error != PE_NO_ERROR)
 		{
 			*in = old_in;
 			free_expr(&result);
@@ -1312,24 +1298,24 @@ parse_funccall(const char **in)
 		}
 	}
 
-	skip_whitespace_tokens(in);
-	if(last_token.type != RPAREN)
+	skip_whitespace_tokens(ctx, in);
+	if(ctx->last_token.type != RPAREN)
 	{
-		last_error = PE_INVALID_EXPRESSION;
+		ctx->last_error = PE_INVALID_EXPRESSION;
 	}
-	get_next(in);
+	get_next(ctx, in);
 
 	return result;
 }
 
 /* arglist ::= or_expr { ',' or_expr } */
 static void
-parse_arglist(const char **in, expr_t *call_expr)
+parse_arglist(parse_context_t *ctx, const char **in, expr_t *call_expr)
 {
 	do
 	{
-		const expr_t op = parse_or_expr(in);
-		if(last_error != PE_NO_ERROR)
+		const expr_t op = parse_or_expr(ctx, in);
+		if(ctx->last_error != PE_NO_ERROR)
 		{
 			free_expr(&op);
 			break;
@@ -1337,23 +1323,23 @@ parse_arglist(const char **in, expr_t *call_expr)
 
 		if(add_expr_op(call_expr, &op) != 0)
 		{
-			last_error = PE_INTERNAL;
+			ctx->last_error = PE_INTERNAL;
 			break;
 		}
 
-		skip_whitespace_tokens(in);
+		skip_whitespace_tokens(ctx, in);
 
-		if(last_token.type != COMMA)
+		if(ctx->last_token.type != COMMA)
 		{
 			break;
 		}
-		get_next(in);
+		get_next(ctx, in);
 	}
-	while(last_error == PE_NO_ERROR);
+	while(ctx->last_error == PE_NO_ERROR);
 
-	if(last_error == PE_INVALID_EXPRESSION)
+	if(ctx->last_error == PE_INVALID_EXPRESSION)
 	{
-		last_error = PE_INVALID_SUBEXPRESSION;
+		ctx->last_error = PE_INVALID_SUBEXPRESSION;
 	}
 }
 
@@ -1361,22 +1347,22 @@ parse_arglist(const char **in, expr_t *call_expr)
 
 /* Skips series of consecutive whitespace. */
 static void
-skip_whitespace_tokens(const char **in)
+skip_whitespace_tokens(parse_context_t *ctx, const char **in)
 {
-	while(last_token.type == WHITESPACE)
+	while(ctx->last_token.type == WHITESPACE)
 	{
-		get_next(in);
+		get_next(ctx, in);
 	}
 }
 
-/* Gets next token from input.  Configures last_token global variable. */
+/* Gets next token from input.  Configures last_token variable. */
 static void
-get_next(const char **in)
+get_next(parse_context_t *ctx, const char **in)
 {
 	const char *const start = *in;
 	TOKENS_TYPE tt;
 
-	if(last_token.type == END)
+	if(ctx->last_token.type == END)
 		return;
 
 	switch((*in)[0])
@@ -1482,40 +1468,40 @@ get_next(const char **in)
 			tt = SYM;
 			break;
 	}
-	prev_token = last_token;
-	last_token.c = **in;
-	last_token.type = tt;
+	ctx->prev_token = ctx->last_token;
+	ctx->last_token.c = **in;
+	ctx->last_token.type = tt;
 
 	if(tt != END)
 		++*in;
 
-	strncpy(last_token.str, start, *in - start);
-	last_token.str[*in - start] = '\0';
+	strncpy(ctx->last_token.str, start, *in - start);
+	ctx->last_token.str[*in - start] = '\0';
 }
 
 void
-report_parsing_error(ParsingErrors error)
+vle_parser_report(const parsing_result_t *result)
 {
-	switch(error)
+	switch(result->error)
 	{
 		case PE_NO_ERROR:
 			/* Not an error. */
 			break;
 		case PE_INVALID_EXPRESSION:
 			vle_tb_append_linef(vle_err, "%s: %s", "Invalid expression",
-					get_last_position());
+					result->last_position);
 			break;
 		case PE_INVALID_SUBEXPRESSION:
 			vle_tb_append_linef(vle_err, "%s: %s", "Invalid subexpression",
-					get_last_position());
+					result->last_position);
 			break;
 		case PE_MISSING_QUOTE:
 			vle_tb_append_linef(vle_err, "%s: %s",
-					"Expression is missing closing quote", get_last_position());
+					"Expression is missing closing quote", result->last_position);
 			break;
 		case PE_MISSING_PAREN:
 			vle_tb_append_linef(vle_err, "%s: %s",
-					"Expression is missing closing parenthesis", get_last_position());
+					"Expression is missing closing parenthesis", result->last_position);
 			break;
 		case PE_INTERNAL:
 			vle_tb_append_line(vle_err, "Internal error");
diff --git a/src/engine/parsing.h b/src/engine/parsing.h
index 04644e0..67a7d60 100644
--- a/src/engine/parsing.h
+++ b/src/engine/parsing.h
@@ -33,6 +33,23 @@ typedef enum
 }
 ParsingErrors;
 
+/* Describes result of parsing and evaluating an expression. */
+typedef struct
+{
+	/* Result of evaluation. */
+	var_t value;
+	/* Actual position in a string, where parser has stopped. */
+	const char *last_parsed_char;
+	/* Logical (e.g. beginning of wrong expression) position in a string,
+	 * where parser has stopped. */
+	const char *last_position;
+	/* Non-zero if the penultimate read token was whitespace. */
+	char ends_with_whitespace;
+	/* Error code. */
+	ParsingErrors error;
+}
+parsing_result_t;
+
 /* A type of function that will be used to resolve environment variable
  * value. If variable doesn't exist the function should return an empty
  * string. The function should not allocate new string. */
@@ -42,28 +59,14 @@ typedef const char * (*getenv_func)(const char *envname);
 typedef void (*print_error_func)(const char msg[]);
 
 /* Can be called several times.  getenv_f can be NULL. */
-void init_parser(getenv_func getenv_f);
-
-/* Returns logical (e.g. beginning of wrong expression) position in a string,
- * where parser has stopped. */
-const char * get_last_position(void);
-
-/* Returns actual position in a string, where parser has stopped. */
-const char * get_last_parsed_char(void);
-
-/* Performs parsing.  After calling this function get_last_position() will
- * return useful information.  Returns error code and puts result of expression
- * evaluation in the result parameter. */
-ParsingErrors parse(const char input[], int interactive, var_t *result);
-
-/* Returns evaluation result, may be used to get value on error. */
-var_t get_parsing_result(void);
+void vle_parser_init(getenv_func getenv_f);
 
-/* Returns non-zero if previously read token was whitespace. */
-int is_prev_token_whitespace(void);
+/* Performs parsing and evaluation.  Returns structure describing the outcome.
+ * Field value of the result should be freed by the caller. */
+parsing_result_t vle_parser_eval(const char input[], int interactive);
 
 /* Appends error message with details to the error stream. */
-void report_parsing_error(ParsingErrors error);
+void vle_parser_report(const parsing_result_t *result);
 
 #endif /* VIFM__ENGINE__PARSING_H__ */
 
diff --git a/src/engine/variables.c b/src/engine/variables.c
index e1962f4..3ef3255 100644
--- a/src/engine/variables.c
+++ b/src/engine/variables.c
@@ -142,16 +142,23 @@ init_variables(void)
 
 	free_string_array(env_lst, env_count);
 
-	init_parser(&local_getenv);
+	vle_parser_init(&local_getenv);
 
 	initialized = 1;
 }
 
 const char *
 local_getenv(const char envname[])
+{
+	const char *value = local_getenv_null(envname);
+	return (value == NULL ? "" : value);
+}
+
+const char *
+local_getenv_null(const char envname[])
 {
 	envvar_t *record = find_record(envname);
-	return (record == NULL || record->removed) ? "" : record->val;
+	return (record == NULL || record->removed) ? NULL : record->val;
 }
 
 /* Initializes environment variable inherited from parent process. */
@@ -231,9 +238,7 @@ let_variables(const char cmd[])
 {
 	char name[VAR_NAME_MAX + 1];
 	int error;
-	var_t res_var;
 	char *str_val;
-	ParsingErrors parsing_error;
 	VariableType type;
 	VariableOperation op;
 
@@ -253,34 +258,37 @@ let_variables(const char cmd[])
 
 	cmd = skip_whitespace(cmd);
 
-	parsing_error = parse(cmd, 1, &res_var);
-	if(parsing_error != PE_NO_ERROR)
+	parsing_result_t result = vle_parser_eval(cmd, /*interactive=*/1);
+	if(result.error != PE_NO_ERROR)
 	{
-		report_parsing_error(parsing_error);
-		return -1;
+		vle_parser_report(&result);
+		goto fail;
 	}
 
-	if(get_last_position() != NULL && *get_last_position() != '\0')
+	if(result.last_position != NULL && result.last_position[0] != '\0')
 	{
 		vle_tb_append_linef(vle_err, "%s: %s", "Incorrect :let statement",
 				"trailing characters");
-		return -1;
+		goto fail;
 	}
 
 	if(!is_valid_op(name, type, op))
 	{
 		vle_tb_append_linef(vle_err, "Wrong variable type for this operation");
-		return -1;
+		goto fail;
 	}
 
-	str_val = var_to_str(res_var);
+	str_val = var_to_str(result.value);
 
 	error = perform_op(name, type, op, str_val);
 
 	free(str_val);
-	var_free(res_var);
-
+	var_free(result.value);
 	return error;
+
+fail:
+	var_free(result.value);
+	return -1;
 }
 
 /* Extracts name from the string.  Returns zero on success, otherwise non-zero
diff --git a/src/engine/variables.h b/src/engine/variables.h
index 172de2b..9f96955 100644
--- a/src/engine/variables.h
+++ b/src/engine/variables.h
@@ -37,6 +37,10 @@ void init_variables(void);
  * requested variable doesn't exist. */
 const char * local_getenv(const char envname[]);
 
+/* Gets cached value of environment variable envname.  Returns NULL if requested
+ * variable doesn't exist. */
+const char * local_getenv_null(const char envname[]);
+
 /* Gets variables value by its name.  Returns the value (not a copy), which is
  * var_error() in case requested variable doesn't exist. */
 var_t getvar(const char varname[]);
diff --git a/src/event_loop.c b/src/event_loop.c
index f26c9e0..08817d6 100644
--- a/src/event_loop.c
+++ b/src/event_loop.c
@@ -35,6 +35,7 @@
 #include "engine/completion.h"
 #include "engine/keys.h"
 #include "engine/mode.h"
+#include "lua/vlua.h"
 #include "modes/dialogs/msg_dialog.h"
 #include "modes/modes.h"
 #include "modes/wk.h"
@@ -150,6 +151,15 @@ event_loop(const int *quit, int manage_marking)
 
 			bg_check();
 
+			/* Lua might not be initialized in tests. */
+			if(input_buf_pos == 0 && !wait_for_enter && vle_mode_is(NORMAL_MODE) &&
+					curr_stats.vlua != NULL)
+			{
+				/* We're not waiting for anything, so side-effects of callbacks
+				 * shouldn't be disruptive to the user. */
+				vlua_process_callbacks(curr_stats.vlua);
+			}
+
 			got_input = (get_char_async_loop(status_bar, &c, actual_timeout) != ERR);
 
 			/* If suggestion delay timed out, reset it and wait the rest of the
@@ -214,7 +224,7 @@ event_loop(const int *quit, int manage_marking)
 			{
 				/* Recover from input buffer overflow by resetting its contents. */
 				reset_input_buf(input_buf, &input_buf_pos);
-				clear_input_bar();
+				modes_input_bar_clear();
 				continue;
 			}
 		}
@@ -272,7 +282,7 @@ event_loop(const int *quit, int manage_marking)
 
 				if(got_input)
 				{
-					modupd_input_bar(input_buf);
+					modes_input_bar_update(input_buf);
 				}
 
 				if(last_result == KEYS_WAIT_SHORT && wcscmp(input_buf, L"\033") == 0)
@@ -282,14 +292,10 @@ event_loop(const int *quit, int manage_marking)
 
 				if(counter > 0)
 				{
-					clear_input_bar();
+					modes_input_bar_clear();
 				}
 
-				if(!curr_stats.save_msg && curr_view->selected_files &&
-						!vle_mode_is(CMDLINE_MODE))
-				{
-					print_selected_msg();
-				}
+				modes_statusbar_update();
 				continue;
 			}
 		}
@@ -299,7 +305,7 @@ event_loop(const int *quit, int manage_marking)
 		process_scheduled_updates();
 
 		reset_input_buf(input_buf, &input_buf_pos);
-		clear_input_bar();
+		modes_input_bar_clear();
 
 		if(ui_sb_multiline())
 		{
@@ -566,9 +572,9 @@ static int
 should_check_views_for_changes(void)
 {
 	return !ui_sb_multiline()
-	    && !is_in_menu_like_mode()
+	    && !modes_is_menu_like()
 	    && !modes_is_dialog_like()
-	    && !vle_mode_is(CMDLINE_MODE)
+	    && !modes_is_cmdline_like()
 	    && !suggestions_are_visible;
 }
 
diff --git a/src/filelist.c b/src/filelist.c
index e4d3e53..1e63c11 100644
--- a/src/filelist.c
+++ b/src/filelist.c
@@ -105,6 +105,7 @@ static void init_view_history(view_t *view);
 static int navigate_to_file_in_custom_view(view_t *view, const char dir[],
 		const char file[]);
 static int fill_dir_entry_by_path(dir_entry_t *entry, const char path[]);
+static void on_custom_view_leave(view_t *view);
 #ifndef _WIN32
 static int fill_dir_entry(dir_entry_t *entry, const char path[],
 		const struct dirent *d);
@@ -172,8 +173,8 @@ static entries_t flist_list_in(view_t *view, const char path[], int only_dirs,
 		int can_include_parent);
 static dir_entry_t * pick_sibling(view_t *view, entries_t parent_dirs,
 		int offset, int wrap, int *wrapped);
-static int iter_entries(view_t *view, dir_entry_t **entry,
-		entry_predicate pred);
+static int iter_entries(view_t *view, dir_entry_t **entry, entry_predicate pred,
+		int valid_only);
 static int mark_selected(view_t *view);
 static int set_position_by_path(view_t *view, const char path[]);
 static int flist_load_tree_internal(view_t *view, const char path[], int reload,
@@ -184,8 +185,7 @@ static void tree_from_cv(view_t *view);
 static int complete_tree(const char name[], int valid, const void *parent_data,
 		void *data, void *arg);
 static void reset_entry_list(view_t *view, dir_entry_t **entries, int *count);
-static void drop_tops(view_t *view, dir_entry_t *entries, int *nentries,
-		int extra);
+static void drop_tops(dir_entry_t *entries, int *nentries, int extra);
 static int add_files_recursively(view_t *view, const char path[],
 		trie_t *excluded_paths, trie_t *folded_paths, int parent_pos,
 		int no_direct_parent, int depth);
@@ -260,8 +260,8 @@ flist_free_view(view_t *view)
 	/* For the application, we don't need to zero out fields after freeing them,
 	 * but doing so allows reusing this function in tests. */
 
-	free_dir_entries(view, &view->dir_entry, &view->list_rows);
-	free_dir_entries(view, &view->custom.entries, &view->custom.entry_count);
+	free_dir_entries(&view->dir_entry, &view->list_rows);
+	free_dir_entries(&view->custom.entries, &view->custom.entry_count);
 
 	update_string(&view->custom.next_title, NULL);
 	update_string(&view->custom.orig_dir, NULL);
@@ -273,8 +273,7 @@ flist_free_view(view_t *view)
 	view->custom.folded_paths = NULL;
 	view->custom.paths_cache = NULL;
 
-	free_dir_entries(view, &view->custom.full.entries,
-			&view->custom.full.nentries);
+	free_dir_entries(&view->custom.full.entries, &view->custom.full.nentries);
 
 	/* Two pointer fields below don't contain valid data that needs to be freed,
 	 * zeroing them for tests and to at least mention them to signal that they
@@ -304,8 +303,8 @@ flist_free_view(view_t *view)
 
 	update_string(&view->last_dir, NULL);
 
-	flist_free_cache(view, &view->left_column);
-	flist_free_cache(view, &view->right_column);
+	flist_free_cache(&view->left_column);
+	flist_free_cache(&view->right_column);
 
 	update_string(&view->last_curr_file, NULL);
 
@@ -717,26 +716,9 @@ change_directory(view_t *view, const char directory[])
 		flist_hist_setup(view, NULL, "", -1, -1);
 	}
 
-	/* Perform additional actions on leaving custom view. */
 	if(was_in_custom_view)
 	{
-		if(ui_view_unsorted(view))
-		{
-			enable_view_sorting(view);
-		}
-		if(cv_compare(view->custom.type))
-		{
-			view_t *const other = (view == curr_view) ? other_view : curr_view;
-
-			/* Indicate that this is not a compare view anymore. */
-			view->custom.type = CV_REGULAR;
-
-			/* Leave compare mode in both views at the same time. */
-			if(other->custom.type == CV_DIFF)
-			{
-				rn_leave(other, 1);
-			}
-		}
+		on_custom_view_leave(view);
 	}
 
 	if(location_changed || was_in_custom_view)
@@ -750,6 +732,36 @@ change_directory(view_t *view, const char directory[])
 	return 0;
 }
 
+/* Performs additional actions on leaving custom view. */
+static void
+on_custom_view_leave(view_t *view)
+{
+	if(ui_view_unsorted(view))
+	{
+		enable_view_sorting(view);
+	}
+
+	if(cv_compare(view->custom.type))
+	{
+		view_t *const other = (view == curr_view) ? other_view : curr_view;
+
+		/* Indicate that this is not a compare view anymore. */
+		view->custom.type = CV_REGULAR;
+
+		/* Leave compare mode in both views at the same time. */
+		if(other->custom.type == CV_DIFF)
+		{
+			rn_leave(other, 1);
+		}
+	}
+
+	trie_free(view->custom.excluded_paths);
+	view->custom.excluded_paths = NULL;
+
+	trie_free(view->custom.folded_paths);
+	view->custom.folded_paths = NULL;
+}
+
 int
 is_dir_list_loaded(view_t *view)
 {
@@ -840,7 +852,7 @@ flist_custom_active(const view_t *view)
 void
 flist_custom_start(view_t *view, const char title[])
 {
-	free_dir_entries(view, &view->custom.entries, &view->custom.entry_count);
+	free_dir_entries(&view->custom.entries, &view->custom.entry_count);
 	(void)replace_string(&view->custom.next_title, title);
 
 	trie_free(view->custom.paths_cache);
@@ -1069,7 +1081,7 @@ flist_custom_finish_internal(view_t *view, CVType type, int reload,
 
 	if(empty_view && !allow_empty)
 	{
-		free_dir_entries(view, &view->custom.entries, &view->custom.entry_count);
+		free_dir_entries(&view->custom.entries, &view->custom.entry_count);
 		update_string(&view->custom.next_title, NULL);
 		return 1;
 	}
@@ -1111,7 +1123,7 @@ flist_custom_finish_internal(view_t *view, CVType type, int reload,
 	}
 
 	/* Replace view file list with custom list. */
-	free_dir_entries(view, &view->dir_entry, &view->list_rows);
+	free_dir_entries(&view->dir_entry, &view->list_rows);
 	view->dir_entry = view->custom.entries;
 	view->list_rows = view->custom.entry_count;
 	view->custom.entries = NULL;
@@ -1398,8 +1410,8 @@ flist_custom_clone(view_t *to, const view_t *from, int as_tree)
 		++j;
 	}
 
-	free_dir_entries(to, &to->custom.entries, &to->custom.entry_count);
-	free_dir_entries(to, &to->dir_entry, &to->list_rows);
+	free_dir_entries(&to->custom.entries, &to->custom.entry_count);
+	free_dir_entries(&to->dir_entry, &to->list_rows);
 	to->dir_entry = dst;
 	to->list_rows = j;
 
@@ -1434,7 +1446,7 @@ flist_custom_uncompress_tree(view_t *view)
 
 		if(entries[i].child_pos == 0 && is_parent_dir(entries[i].name))
 		{
-			fentry_free(view, &entries[i]);
+			fentry_free(&entries[i]);
 			restore_parent = 1;
 			continue;
 		}
@@ -1452,7 +1464,7 @@ flist_custom_uncompress_tree(view_t *view)
 	fsdata_free(tree);
 	dynarray_free(entries);
 
-	drop_tops(view, view->dir_entry, &view->list_rows, 0);
+	drop_tops(view->dir_entry, &view->list_rows, 0);
 
 	if(restore_parent)
 	{
@@ -1488,8 +1500,7 @@ flist_custom_save(view_t *view)
 static void
 flist_custom_drop_save(view_t *view)
 {
-	free_dir_entries(view, &view->custom.full.entries,
-			&view->custom.full.nentries);
+	free_dir_entries(&view->custom.full.entries, &view->custom.full.nentries);
 }
 
 const char *
@@ -1701,8 +1712,8 @@ populate_dir_list_internal(view_t *view, int reload)
 	 * date with main column. */
 	if(reload)
 	{
-		flist_free_cache(view, &view->left_column);
-		flist_free_cache(view, &view->right_column);
+		flist_free_cache(&view->left_column);
+		flist_free_cache(&view->right_column);
 	}
 
 	if(flist_custom_active(view))
@@ -1710,12 +1721,9 @@ populate_dir_list_internal(view_t *view, int reload)
 		return populate_custom_view(view, reload);
 	}
 
-	if(!reload && is_dir_big(view->curr_dir))
+	if(!reload && is_dir_big(view->curr_dir) && !modes_is_cmdline_like())
 	{
-		if(!vle_mode_is(CMDLINE_MODE))
-		{
-			ui_sb_quick_msgf("%s", "Reading directory...");
-		}
+		ui_sb_quick_msgf("%s", "Reading directory...");
 	}
 
 	if(curr_stats.load_stage < 2)
@@ -1774,7 +1782,7 @@ populate_dir_list_internal(view_t *view, int reload)
 		add_parent_dir(view);
 	}
 
-	if(!reload && !vle_mode_is(CMDLINE_MODE))
+	if(!reload && !modes_is_cmdline_like())
 	{
 		ui_sb_clear();
 	}
@@ -1982,7 +1990,7 @@ zap_compare_view(view_t *view, view_t *other, zap_filter filter, void *arg)
 			const int separator = find_separator(other, i);
 			if(separator >= 0)
 			{
-				fentry_free(view, entry);
+				fentry_free(entry);
 				other->dir_entry[separator].temporary = 1;
 
 				if(view->list_pos == i)
@@ -2167,7 +2175,7 @@ zap_entries(view_t *view, dir_entry_t *entries, int *count, zap_filter filter,
 		int k;
 		for(k = 0; k < nremoved; ++k)
 		{
-			fentry_free(view, &entry[k]);
+			fentry_free(&entry[k]);
 		}
 
 		/* If we're removing file from main list of entries and cursor is right on
@@ -2272,7 +2280,7 @@ is_dir_big(const char path[])
 static void
 free_view_entries(view_t *view)
 {
-	free_dir_entries(view, &view->dir_entry, &view->list_rows);
+	free_dir_entries(&view->dir_entry, &view->list_rows);
 }
 
 /* Updates file list with files from current directory.  Returns zero on
@@ -2288,7 +2296,7 @@ update_dir_list(view_t *view, int reload)
 	if(enum_dir_content(view->curr_dir, &add_file_entry_to_view, view) != 0)
 	{
 		LOG_SERROR_MSG(errno, "Can't opendir() \"%s\"", view->curr_dir);
-		free_dir_entries(view, &prev_dir_entries, &prev_list_rows);
+		free_dir_entries(&prev_dir_entries, &prev_list_rows);
 		return 1;
 	}
 
@@ -2340,7 +2348,7 @@ finish_dir_list_change(view_t *view, dir_entry_t *entries, int len)
 	if(entries != NULL)
 	{
 		merge_lists(view, entries, len);
-		free_dir_entries(view, &entries, &len);
+		free_dir_entries(&entries, &len);
 	}
 
 	view->dir_entry = dynarray_shrink(view->dir_entry);
@@ -2381,7 +2389,7 @@ add_file_entry_to_view(const char name[], const void *data, void *param)
 	}
 	else
 	{
-		fentry_free(view, entry);
+		fentry_free(entry);
 	}
 
 	return 0;
@@ -2411,14 +2419,14 @@ resort_dir_list(int msg, view_t *view)
 static void
 sort_dir_list(int msg, view_t *view)
 {
-	if(msg && view->list_rows > 2048 && !vle_mode_is(CMDLINE_MODE))
+	if(msg && view->list_rows > 2048 && !modes_is_cmdline_like())
 	{
 		ui_sb_quick_msgf("%s", "Sorting directory...");
 	}
 
 	sort_view(view);
 
-	if(msg && !vle_mode_is(CMDLINE_MODE))
+	if(msg && !modes_is_cmdline_like())
 	{
 		ui_sb_clear();
 	}
@@ -2703,23 +2711,23 @@ replace_dir_entries(view_t *view, dir_entry_t **entries, int *count,
 		if(entry->name == NULL || entry->origin == NULL)
 		{
 			int count_so_far = i + 1;
-			free_dir_entries(view, &new, &count_so_far);
+			free_dir_entries(&new, &count_so_far);
 			return;
 		}
 	}
 
-	free_dir_entries(view, entries, count);
+	free_dir_entries(entries, count);
 	*entries = new;
 	*count = with_count;
 }
 
 void
-free_dir_entries(view_t *view, dir_entry_t **entries, int *count)
+free_dir_entries(dir_entry_t **entries, int *count)
 {
 	int i;
 	for(i = 0; i < *count; ++i)
 	{
-		fentry_free(view, &(*entries)[i]);
+		fentry_free(&(*entries)[i]);
 	}
 
 	dynarray_free(*entries);
@@ -2728,7 +2736,7 @@ free_dir_entries(view_t *view, dir_entry_t **entries, int *count)
 }
 
 void
-fentry_free(const view_t *view, dir_entry_t *entry)
+fentry_free(dir_entry_t *entry)
 {
 	free(entry->name);
 	entry->name = NULL;
@@ -2772,7 +2780,7 @@ entry_list_add(view_t *view, dir_entry_t **list, int *list_size,
 
 	if(fill_dir_entry_by_path(dir_entry, path) != 0)
 	{
-		fentry_free(view, dir_entry);
+		fentry_free(dir_entry);
 		return NULL;
 	}
 
@@ -2914,7 +2922,7 @@ flist_update_cache(view_t *view, cached_entries_t *cache, const char path[])
 		{
 			/* Reset the cache on failure to create a watcher to do not accidentally
 			 * provide incorrect data. */
-			flist_free_cache(view, cache);
+			flist_free_cache(cache);
 			return 0;
 		}
 
@@ -2925,7 +2933,7 @@ flist_update_cache(view_t *view, cached_entries_t *cache, const char path[])
 
 	if(poll_watcher(cache->watch, path) != FSWS_UNCHANGED || update)
 	{
-		free_dir_entries(view, &cache->entries.entries, &cache->entries.nentries);
+		free_dir_entries(&cache->entries.entries, &cache->entries.nentries);
 		cache->entries = flist_list_in(view, path, 0, 1);
 		return 1;
 	}
@@ -2958,9 +2966,9 @@ poll_watcher(fswatch_t *watch, const char path[])
 }
 
 void
-flist_free_cache(view_t *view, cached_entries_t *cache)
+flist_free_cache(cached_entries_t *cache)
 {
-	free_dir_entries(view, &cache->entries.entries, &cache->entries.nentries);
+	free_dir_entries(&cache->entries.entries, &cache->entries.nentries);
 	update_string(&cache->dir, NULL);
 	fswatch_free(cache->watch);
 	cache->watch = NULL;
@@ -2983,7 +2991,7 @@ flist_update_origins(view_t *view)
 void
 flist_toggle_fold(view_t *view)
 {
-	if(!cv_tree(view->custom.type))
+	if(!flist_custom_active(view) || !cv_tree(view->custom.type))
 	{
 		return;
 	}
@@ -3033,7 +3041,7 @@ remove_child_entries(view_t *view, dir_entry_t *entry)
 	int i;
 	for(i = 0; i < child_count; ++i)
 	{
-		fentry_free(view, &entry[1 + i]);
+		fentry_free(&entry[1 + i]);
 	}
 
 	fix_tree_links(view->dir_entry, entry, pos, pos, 0, -child_count);
@@ -3092,7 +3100,7 @@ load_saving_pos(view_t *view)
 
 	fview_cursor_redraw(view);
 
-	if(curr_stats.number_of_windows != 1 || view == curr_view)
+	if(ui_view_is_visible(view))
 	{
 		refresh_view_win(view);
 	}
@@ -3101,7 +3109,7 @@ load_saving_pos(view_t *view)
 int
 window_shows_dirlist(const view_t *view)
 {
-	if(curr_stats.number_of_windows == 1 && view == other_view)
+	if(!ui_view_is_visible(view))
 	{
 		return 0;
 	}
@@ -3303,7 +3311,7 @@ go_to_sibling_dir(view_t *view, int offset, int wrap)
 		}
 	}
 
-	free_dir_entries(view, &parent_dirs.entries, &parent_dirs.nentries);
+	free_dir_entries(&parent_dirs.entries, &parent_dirs.nentries);
 	return save_msg;
 }
 
@@ -3387,7 +3395,7 @@ flist_list_in(view_t *view, const char path[], int only_dirs,
 		if((only_dirs && !is_dir) ||
 				!filters_file_is_visible(view, path, list[i], is_dir, 0))
 		{
-			fentry_free(view, entry);
+			fentry_free(entry);
 			--siblings.nentries;
 		}
 	}
@@ -3498,27 +3506,28 @@ fentry_get_size(const view_t *view, const dir_entry_t *entry)
 int
 iter_selected_entries(view_t *view, dir_entry_t **entry)
 {
-	return iter_entries(view, entry, &is_entry_selected);
+	return iter_entries(view, entry, &is_entry_selected, /*valid_only=*/1);
 }
 
 int
 iter_marked_entries(view_t *view, dir_entry_t **entry)
 {
-	return iter_entries(view, entry, &is_entry_marked);
+	return iter_entries(view, entry, &is_entry_marked, /*valid_only=*/1);
 }
 
 /* Implements iteration over the entries which match specific predicate.
  * Returns non-zero if matching entry is found and is loaded to *entry,
  * otherwise it's set to NULL and zero is returned. */
 static int
-iter_entries(view_t *view, dir_entry_t **entry, entry_predicate pred)
+iter_entries(view_t *view, dir_entry_t **entry, entry_predicate pred,
+		int valid_only)
 {
 	int next = (*entry == NULL) ? 0 : (*entry - view->dir_entry + 1);
 
 	while(next < view->list_rows)
 	{
 		dir_entry_t *const e = &view->dir_entry[next];
-		if(fentry_is_valid(e) && pred(e))
+		if((!valid_only || fentry_is_valid(e)) && pred(e))
 		{
 			*entry = e;
 			return 1;
@@ -3554,6 +3563,18 @@ iter_selection_or_current(view_t *view, dir_entry_t **entry)
 	return iter_selected_entries(view, entry);
 }
 
+int
+iter_selection_or_current_any(view_t *view, dir_entry_t **entry)
+{
+	if(view->selected_files == 0)
+	{
+		dir_entry_t *const curr = get_current_entry(view);
+		*entry = (*entry == NULL ? curr : NULL);
+		return *entry != NULL;
+	}
+	return iter_entries(view, entry, &is_entry_selected, /*valid_only=*/0);
+}
+
 int
 entry_to_pos(const view_t *view, const dir_entry_t *entry)
 {
@@ -3888,13 +3909,24 @@ fentry_points_to(const dir_entry_t *entry, const char path[])
 	char previewed[PATH_MAX + 1];
 	get_full_path_of(entry, sizeof(previewed), previewed);
 
-	if(entry->type == FT_LINK)
+	if(paths_are_equal(path, previewed))
+	{
+		return 1;
+	}
+
+	/* Can also check resolved paths for symbolic links. */
+	if(entry->type != FT_LINK)
 	{
-		/* Failure won't change the buffer. */
-		(void)get_link_target_abs(previewed, entry->origin, previewed,
-				sizeof(previewed));
+		/* Nothing more to do for a non-link. */
+		return 0;
 	}
 
+	if(get_link_target_abs(previewed, entry->origin, previewed,
+				sizeof(previewed)) != 0)
+	{
+		/* We don't know. */
+		return 0;
+	}
 	return paths_are_equal(path, previewed);
 }
 
@@ -4078,7 +4110,7 @@ tree_from_cv(view_t *view)
 		}
 		else
 		{
-			fentry_free(view, &entries[i]);
+			fentry_free(&entries[i]);
 		}
 	}
 
@@ -4090,7 +4122,7 @@ tree_from_cv(view_t *view)
 	fsdata_free(tree);
 	dynarray_free(entries);
 
-	drop_tops(view, view->custom.entries, &view->custom.entry_count, 1);
+	drop_tops(view->custom.entries, &view->custom.entry_count, 1);
 }
 
 /* fsdata_traverse() callback that flattens the tree into array of entries.
@@ -4186,14 +4218,14 @@ complete_tree(const char name[], int valid, const void *parent_data, void *data,
 static void
 reset_entry_list(view_t *view, dir_entry_t **entries, int *count)
 {
-	free_dir_entries(view, entries, count);
+	free_dir_entries(entries, count);
 	add_parent_entry(view, entries, count);
 }
 
 /* Traverses root children and drops fake root nodes and optionally extra tops
  * of subtrees. */
 static void
-drop_tops(view_t *view, dir_entry_t *entries, int *nentries, int extra)
+drop_tops(dir_entry_t *entries, int *nentries, int extra)
 {
 	int i;
 	for(i = 0; i < *nentries - 1; i += entries[i].child_count + 1)
@@ -4203,7 +4235,7 @@ drop_tops(view_t *view, dir_entry_t *entries, int *nentries, int extra)
 				((extra && entries[j].child_count == entries[j + 1].child_count + 1) ||
 				 entries[j].name[0] == '\0') && entries[j].tag != 1)
 		{
-			fentry_free(view, &entries[j++]);
+			fentry_free(&entries[j++]);
 		}
 
 		memmove(&entries[i], &entries[j], sizeof(*entries)*(*nentries - j));
@@ -4464,7 +4496,7 @@ init_parent_entry(view_t *view, dir_entry_t *entry, const char path[])
 	/* Load the inode info or leave blank values in entry. */
 	if(os_lstat(path, &s) != 0)
 	{
-		fentry_free(view, entry);
+		fentry_free(entry);
 		LOG_SERROR_MSG(errno, "Can't lstat() \"%s\"", path);
 		log_cwd();
 		return 1;
diff --git a/src/filelist.h b/src/filelist.h
index aaa1019..725d8ca 100644
--- a/src/filelist.h
+++ b/src/filelist.h
@@ -204,6 +204,8 @@ int iter_marked_entries(view_t *view, dir_entry_t **entry);
 /* Same as iter_selected_entries() function, but when selection is absent
  * current file is processed. */
 int iter_selection_or_current(view_t *view, dir_entry_t **entry);
+/* Version of iter_selected_entries() that doesn't exclude invalid entries. */
+int iter_selection_or_current_any(view_t *view, dir_entry_t **entry);
 /* Maps one of file list entries to its position in the list.  Returns the
  * position or -1 on wrong entry. */
 int entry_to_pos(const view_t *view, const dir_entry_t *entry);
@@ -275,11 +277,10 @@ dir_entry_t * add_dir_entry(dir_entry_t **list, size_t *list_size,
  * entry or NULL on error. */
 dir_entry_t * entry_list_add(view_t *view, dir_entry_t **list, int *list_size,
 		const char path[]);
-/* Frees list of directory entries related to the view.  Sets *entries and
- * *count to safe values. */
-void free_dir_entries(view_t *view, dir_entry_t **entries, int *count);
+/* Frees list of directory entries.  Sets *entries and *count to safe values. */
+void free_dir_entries(dir_entry_t **entries, int *count);
 /* Frees single directory entry. */
-void fentry_free(const view_t *view, dir_entry_t *entry);
+void fentry_free(dir_entry_t *entry);
 /* Adds parent directory entry (..) to filelist. */
 void add_parent_dir(view_t *view);
 /* Changes name of a file entry, performing additional required updates. */
@@ -310,7 +311,7 @@ int flist_clone_tree(view_t *to, const view_t *from);
 int flist_update_cache(view_t *view, cached_entries_t *cache,
 		const char path[]);
 /* Frees the cache. */
-void flist_free_cache(view_t *view, cached_entries_t *cache);
+void flist_free_cache(cached_entries_t *cache);
 /* Updates non-heap-allocated origin pointers of entries in file list
  * entries. */
 void flist_update_origins(view_t *view);
diff --git a/src/filtering.c b/src/filtering.c
index fd3818b..c4d907f 100644
--- a/src/filtering.c
+++ b/src/filtering.c
@@ -367,7 +367,7 @@ filters_drop_temporaries(view_t *view, dir_entry_t entries[])
 
 		if(entry->temporary)
 		{
-			fentry_free(view, entry);
+			fentry_free(entry);
 			continue;
 		}
 
@@ -559,7 +559,7 @@ update_filtering_lists(view_t *view, int add, int clear)
 			{
 				if(clear)
 				{
-					fentry_free(view, entry);
+					fentry_free(entry);
 				}
 				continue;
 			}
@@ -592,7 +592,7 @@ update_filtering_lists(view_t *view, int add, int clear)
 		{
 			if(clear)
 			{
-				fentry_free(view, entry);
+				fentry_free(entry);
 			}
 		}
 	}
@@ -604,7 +604,7 @@ update_filtering_lists(view_t *view, int add, int clear)
 		if(!parent_added && parent_entry != NULL && list_size != 0U &&
 				view->dir_entry[0].name != parent_entry->name)
 		{
-			fentry_free(view, parent_entry);
+			fentry_free(parent_entry);
 		}
 	}
 	if(add)
@@ -684,6 +684,22 @@ ensure_filtered_list_not_empty(view_t *view, dir_entry_t *parent_entry)
 	}
 }
 
+void
+local_filter_update_pos(view_t *view)
+{
+	struct local_filter_t *lf = &view->local_filter;
+	if(lf->poshist_len > 0)
+	{
+		const int current_file_pos = lf->in_progress
+		                           ? get_unfiltered_pos(view, view->list_pos)
+		                           : load_unfiltered_list(view);
+		if(current_file_pos >= 0)
+		{
+			lf->poshist[lf->poshist_len - 1] = current_file_pos;
+		}
+	}
+}
+
 void
 local_filter_update_view(view_t *view, int rel_pos)
 {
@@ -762,7 +778,7 @@ find_nearest_neighour(const view_t *view)
 }
 
 void
-local_filter_accept(view_t *view)
+local_filter_accept(view_t *view, int update_history)
 {
 	if(!view->local_filter.in_progress)
 	{
@@ -773,7 +789,10 @@ local_filter_accept(view_t *view)
 
 	local_filter_finish(view);
 
-	hists_filter_save(view->local_filter.filter.raw);
+	if(update_history)
+	{
+		hists_filter_save(view->local_filter.filter.raw);
+	}
 
 	/* Some of previously selected files could be filtered out, update number of
 	 * selected files. */
diff --git a/src/filtering.h b/src/filtering.h
index 8488eff..e88bcd1 100644
--- a/src/filtering.h
+++ b/src/filtering.h
@@ -96,12 +96,15 @@ void name_filters_restore(struct view_t *view);
  * files were filtered out. */
 int local_filter_set(struct view_t *view, const char filter[]);
 
+/* Updates last recorded cursor position. */
+void local_filter_update_pos(struct view_t *view);
+
 /* Updates cursor position and top line of the view according to interactive
  * local filter in progress. */
 void local_filter_update_view(struct view_t *view, int rel_pos);
 
 /* Accepts current value of local filter. */
-void local_filter_accept(struct view_t *view);
+void local_filter_accept(struct view_t *view, int update_history);
 
 /* Sets local filter non-interactively.  List of entries doesn't get updated
  * immediately, an update is scheduled. */
diff --git a/src/flist_hist.c b/src/flist_hist.c
index 7193193..95f20e4 100644
--- a/src/flist_hist.c
+++ b/src/flist_hist.c
@@ -303,14 +303,14 @@ flist_hist_lookup(view_t *view, const view_t *source)
 		const int last = fpos_get_last_visible_cell(view);
 		if(view->list_pos < view->window_cells)
 		{
-			scroll_up(view, view->top_line);
+			fview_scroll_back_by(view, view->top_line);
 		}
 		else if(view->list_pos > last)
 		{
-			scroll_down(view, view->list_pos - last);
+			fview_scroll_fwd_by(view, view->list_pos - last);
 		}
 	}
-	(void)consider_scroll_offset(view);
+	(void)fview_enforce_scroll_offset(view);
 }
 
 int
diff --git a/src/flist_pos.c b/src/flist_pos.c
index 3165163..9589b9c 100644
--- a/src/flist_pos.c
+++ b/src/flist_pos.c
@@ -29,6 +29,7 @@
 #include "ui/fileview.h"
 #include "ui/ui.h"
 #include "utils/fs.h"
+#include "utils/macros.h"
 #include "utils/regexp.h"
 #include "utils/path.h"
 #include "utils/str.h"
@@ -81,7 +82,7 @@ fpos_scroll_down(view_t *view, int lines_count)
 	if(!fpos_are_all_files_visible(view))
 	{
 		view->list_pos =
-			get_corrected_list_pos_down(view, lines_count*view->run_size);
+			fpos_adjust_for_scroll_back(view, lines_count*view->run_size);
 		return 1;
 	}
 	return 0;
@@ -93,12 +94,31 @@ fpos_scroll_up(view_t *view, int lines_count)
 	if(!fpos_are_all_files_visible(view))
 	{
 		view->list_pos =
-			get_corrected_list_pos_up(view, lines_count*view->run_size);
+			fpos_adjust_for_scroll_fwd(view, lines_count*view->run_size);
 		return 1;
 	}
 	return 0;
 }
 
+void
+fpos_scroll_page(view_t *view, int base, int direction)
+{
+	enum { HOR_GAP_SIZE = 2, VER_GAP_SIZE = 1 };
+	int old_pos = view->list_pos;
+	int offset = fview_is_transposed(view)
+	    ? (MAX(1, view->column_count - VER_GAP_SIZE))*view->window_rows
+	    : (view->window_rows - HOR_GAP_SIZE)*view->column_count;
+	int new_pos = base + direction*offset
+	            + old_pos%view->run_size - base%view->run_size;
+	view->list_pos = MAX(0, MIN(view->list_rows - 1, new_pos));
+	fview_scroll_by(view, direction*offset);
+
+	/* Updating list_pos ourselves doesn't take into account
+	 * synchronization/updates of the other view, so trigger them. */
+	ui_view_schedule_redraw(view);
+	fpos_set_pos(view, view->list_pos);
+}
+
 void
 fpos_set_pos(view_t *view, int pos)
 {
@@ -254,22 +274,50 @@ fpos_get_ver_step(const struct view_t *view)
 }
 
 int
-fpos_has_hidden_top(const view_t *view)
+fpos_can_scroll_back(const view_t *view)
+{
+	return (view->top_line > 0);
+}
+
+int
+fpos_can_scroll_fwd(const view_t *view)
 {
-	return (fview_is_transposed(view) ? 0 : can_scroll_up(view));
+	return (fpos_get_last_visible_cell(view) < view->list_rows - 1);
 }
 
 int
-fpos_has_hidden_bottom(const view_t *view)
+fpos_adjust_for_scroll_back(const view_t *view, int pos_delta)
 {
-	return (fview_is_transposed(view) ? 0 : can_scroll_down(view));
+	const int scroll_offset = fpos_get_offset(view);
+	if(view->list_pos <= view->top_line + scroll_offset + (MAX(pos_delta, 1) - 1))
+	{
+		const int column_correction = view->list_pos%view->run_size;
+		const int offset = scroll_offset + pos_delta + column_correction;
+		return view->top_line + offset;
+	}
+	return view->list_pos;
+}
+
+int
+fpos_adjust_for_scroll_fwd(const view_t *view, int pos_delta)
+{
+	const int scroll_offset = fpos_get_offset(view);
+	const int last = fpos_get_last_visible_cell(view);
+	if(view->list_pos >= last - scroll_offset - (MAX(pos_delta, 1) - 1))
+	{
+		const int column_correction = (view->run_size - 1)
+		                            - view->list_pos%view->run_size;
+		const int offset = scroll_offset + pos_delta + column_correction;
+		return last - offset;
+	}
+	return view->list_pos;
 }
 
 int
 fpos_get_top_pos(const view_t *view)
 {
 	return get_column_top_pos(view)
-	     + (can_scroll_up(view) ? fpos_get_offset(view) : 0);
+	     + (fpos_can_scroll_back(view) ? fpos_get_offset(view) : 0);
 }
 
 int
@@ -285,7 +333,7 @@ int
 fpos_get_bottom_pos(const view_t *view)
 {
 	return get_column_bottom_pos(view)
-	     - (can_scroll_down(view) ? fpos_get_offset(view) : 0);
+	     - (fpos_can_scroll_fwd(view) ? fpos_get_offset(view) : 0);
 }
 
 int
@@ -325,7 +373,7 @@ fpos_half_scroll(view_t *view, int down)
 
 	if(down)
 	{
-		new_pos = get_corrected_list_pos_down(view, offset);
+		new_pos = fpos_adjust_for_scroll_back(view, offset);
 		new_pos = MAX(new_pos, view->list_pos + offset);
 
 		if(new_pos >= view->list_rows)
@@ -336,7 +384,7 @@ fpos_half_scroll(view_t *view, int down)
 	}
 	else
 	{
-		new_pos = get_corrected_list_pos_up(view, offset);
+		new_pos = fpos_adjust_for_scroll_fwd(view, offset);
 		new_pos = MIN(new_pos, view->list_pos - offset);
 
 		if(new_pos < 0)
@@ -345,7 +393,7 @@ fpos_half_scroll(view_t *view, int down)
 		}
 	}
 
-	scroll_by_files(view, new_pos - view->list_pos);
+	fview_scroll_by(view, new_pos - view->list_pos);
 	return new_pos;
 }
 
diff --git a/src/flist_pos.h b/src/flist_pos.h
index c62725f..359c641 100644
--- a/src/flist_pos.h
+++ b/src/flist_pos.h
@@ -43,6 +43,10 @@ int fpos_scroll_down(struct view_t *view, int lines_count);
  * position was updated. */
 int fpos_scroll_up(struct view_t *view, int lines_count);
 
+/* Scrolls the view by one view up or down.  The direction should be -1 (up)
+ * or 1 (down). */
+void fpos_scroll_page(struct view_t *view, int base, int direction);
+
 /* Moves cursor to specified position.  Normalizes it if needed, invokes fview
  * update function and can synchronize cursor position in the other view. */
 void fpos_set_pos(struct view_t *view, int pos);
@@ -98,13 +102,21 @@ int fpos_get_hor_step(const struct view_t *view);
  * step. */
 int fpos_get_ver_step(const struct view_t *view);
 
-/* Checks whether there are more elements to show above what can be seen
- * currently.  Returns non-zero if so, otherwise zero is returned. */
-int fpos_has_hidden_top(const struct view_t *view);
+/* Checks whether there are more elements to show above/on the left what can be
+ * seen currently.  Returns non-zero if so, otherwise zero is returned. */
+int fpos_can_scroll_back(const struct view_t *view);
+
+/* Checks whether there are more elements to show below/on the right what can
+ * be seen currently.  Returns non-zero if so, otherwise zero is returned. */
+int fpos_can_scroll_fwd(const struct view_t *view);
 
-/* Checks whether there are more elements to show below what can be seen
- * currently.  Returns non-zero if so, otherwise zero is returned. */
-int fpos_has_hidden_bottom(const struct view_t *view);
+/* Calculates list position corrected for scrolling down.  Returns adjusted
+ * position. */
+int fpos_adjust_for_scroll_back(const struct view_t *view, int pos_delta);
+
+/* Calculates list position corrected for scrolling up.  Returns adjusted
+ * position. */
+int fpos_adjust_for_scroll_fwd(const struct view_t *view, int pos_delta);
 
 /* Calculates position in list of files that corresponds to window top, which is
  * adjusted according to 'scrolloff' option.  Returns the position. */
diff --git a/src/flist_sel.c b/src/flist_sel.c
index 14ab4bd..333088a 100644
--- a/src/flist_sel.c
+++ b/src/flist_sel.c
@@ -37,6 +37,7 @@
 #include "utils/utils.h"
 #include "filelist.h"
 #include "flist_pos.h"
+#include "registers.h"
 #include "running.h"
 
 static void save_selection(view_t *view);
@@ -162,7 +163,7 @@ flist_sel_stash_if_nonempty(view_t *view)
 }
 
 void
-flist_sel_restore(view_t *view, reg_t *reg)
+flist_sel_restore(view_t *view, const reg_t *reg)
 {
 	int i;
 	trie_t *const selection_trie = trie_create(/*free_func=*/NULL);
@@ -245,6 +246,20 @@ flist_sel_by_range(view_t *view, int begin, int end, int select)
 	ui_view_schedule_redraw(view);
 }
 
+void
+flist_sel_by_indexes(view_t *view, int count, const int indexes[], int select)
+{
+	int i;
+	for(i = 0; i < count; ++i)
+	{
+		int idx = indexes[i];
+		dir_entry_t *const entry = &view->dir_entry[idx];
+		select_unselect_entry(view, entry, select);
+	}
+
+	ui_view_schedule_redraw(view);
+}
+
 /* Selects or unselects the entry. */
 static void
 select_unselect_entry(view_t *view, dir_entry_t *entry, int select)
diff --git a/src/flist_sel.h b/src/flist_sel.h
index a1d0597..f4f1eaa 100644
--- a/src/flist_sel.h
+++ b/src/flist_sel.h
@@ -22,8 +22,7 @@
 
 /* This unit provides functions related to selecting items in file lists. */
 
-#include "registers.h"
-
+struct reg_t;
 struct view_t;
 
 /* Clears selection saving it for later use. */
@@ -46,7 +45,7 @@ void flist_sel_stash_if_nonempty(struct view_t *view);
 
 /* Reselects previously selected entries.  When reg is NULL, saved selection is
  * restored, otherwise list of files to restore is taken from the register. */
-void flist_sel_restore(struct view_t *view, reg_t *reg);
+void flist_sel_restore(struct view_t *view, const struct reg_t *reg);
 
 /* Counts number of selected files and writes saves the number in
  * view->selected_files. */
@@ -55,6 +54,11 @@ void flist_sel_recount(struct view_t *view);
 /* Selects or unselects entries in the given range. */
 void flist_sel_by_range(struct view_t *view, int begin, int end, int select);
 
+/* Selects or unselects entries at positions specified in the indexes array of
+ * size count. */
+void flist_sel_by_indexes(struct view_t *view, int count, const int indexes[],
+		int select);
+
 /* Selects or unselects entries that match list of files supplied by external
  * utility.  Returns zero on success, otherwise non-zero is returned and error
  * message is printed on status bar. */
diff --git a/src/fops_common.c b/src/fops_common.c
index 675e465..832ccb5 100644
--- a/src/fops_common.c
+++ b/src/fops_common.c
@@ -21,8 +21,7 @@
 
 #include <regex.h>
 
-#include <sys/stat.h> /* stat umask() */
-#include <sys/types.h> /* mode_t */
+#include <sys/stat.h> /* stat */
 #include <fcntl.h>
 #include <unistd.h> /* unlink() */
 
@@ -33,9 +32,10 @@
 
 #include <assert.h> /* assert() */
 #include <errno.h> /* errno */
+#include <math.h> /* fabsf() sqrtf() */
 #include <stddef.h> /* NULL size_t */
 #include <stdint.h> /* uint64_t */
-#include <stdio.h> /* snprintf() */
+#include <stdio.h> /* FILE snprintf() */
 #include <stdlib.h> /* free() malloc() */
 #include <string.h> /* memset() strcat() strcmp() strdup() strlen() */
 #include <time.h> /* clock_gettime() */
@@ -87,6 +87,21 @@
 /* Key used to switch to progress dialog. */
 #define IO_DETAILS_KEY 'i'
 
+/* Number of entries in history windows used to compute ETA. */
+#define ETA_HISTORY_SIZE 10
+
+/* A circular buffer for computing weighted average. */
+typedef struct
+{
+	/* We're only adding here, so no need to keep head index, because it's zero
+	 * until wrapping and `pos` after wrapping. */
+	int wrapped; /* Whether the buffer has been wrapped. */
+	int pos;     /* Next position for writing (tail). */
+
+	uint64_t entries[ETA_HISTORY_SIZE]; /* Buffer's data. */
+}
+history_window_t;
+
 /* Object for auxiliary information related to progress of operations in
  * io_progress_changed() handler. */
 typedef struct
@@ -107,10 +122,15 @@ typedef struct
 	int progress_bar_max;   /* Width of progress bar during previous call. */
 
 	/* State of rate calculation. */
+	long long start_time;     /* Time of starting the operation. */
 	long long last_calc_time; /* Time of last rate calculation. */
 	uint64_t last_seen_byte;  /* Position at the time of last call. */
-	uint64_t rate;            /* Rate in bytes per millisecond. */
+	float last_rate;          /* Rate in bytes per millisecond. */
+	int last_eta;             /* Last reported ETA. */
 	char *rate_str;           /* Rate formatted as a string. */
+	char *eta_str;            /* ETA formatted as a string. */
+	history_window_t speed_window; /* Smoothing of speed. */
+	history_window_t eta_window;   /* Smoothing of ETA. */
 
 	/* Whether progress is displayed in a dialog, rather than on status bar. */
 	int dialog;
@@ -119,9 +139,18 @@ typedef struct
 }
 progress_data_t;
 
+/* Type of function that returns next weight for weighted average. */
+typedef float (*weight_f)(int idx, float last_weight);
+
 static void io_progress_changed(const io_progress_t *state);
 static int calc_io_progress(const io_progress_t *state, int *skip);
-static void update_io_rate(progress_data_t *pdata, const ioeta_estim_t *estim);
+static void update_io_stats(progress_data_t *pdata, const ioeta_estim_t *estim);
+static void window_add(history_window_t *win, uint64_t value);
+static float window_average(const history_window_t *win, weight_f weight);
+static float linear_weight(int idx, float last_weight);
+static float doubling_weight(int idx, float last_weight);
+static void format_io_stats(progress_data_t *pdata, long long now_ms,
+		uint64_t imm_rate);
 static void update_progress_bar(progress_data_t *pdata,
 		const ioeta_estim_t *estim);
 static void io_progress_fg(const io_progress_t *state, int progress);
@@ -251,28 +280,165 @@ calc_io_progress(const io_progress_t *state, int *skip)
 
 /* Updates rate of operation. */
 static void
-update_io_rate(progress_data_t *pdata, const ioeta_estim_t *estim)
+update_io_stats(progress_data_t *pdata, const ioeta_estim_t *estim)
 {
 	long long current_time_ms = time_in_ms();
 	long long elapsed_time_ms = current_time_ms - pdata->last_calc_time;
 
 	if(elapsed_time_ms == 0 ||
-			(pdata->last_seen_byte != 0 && elapsed_time_ms < 3000))
+			(pdata->last_seen_byte != 0 && elapsed_time_ms < 1000))
 	{
-		/* Rate is updated initially and then once in 3000 milliseconds. */
+		/* Rate is updated initially and then once in 1000 milliseconds. */
 		return;
 	}
 
 	uint64_t bytes_difference = estim->current_byte - pdata->last_seen_byte;
-	pdata->rate = bytes_difference/elapsed_time_ms;
+	uint64_t imm_rate = bytes_difference/elapsed_time_ms;
+
+	/* Smooth immediate rate. */
+	window_add(&pdata->speed_window, imm_rate);
+	float rate = window_average(&pdata->speed_window, &doubling_weight);
+
+	/* Second round of adjusting speed to not deviate from previous value too
+	 * much. */
+	if(rate >= 1 && pdata->last_rate >= 1)
+	{
+		float minV = MIN(rate, pdata->last_rate);
+		float maxV = MAX(rate, pdata->last_rate);
+		rate = pdata->last_rate + (rate - pdata->last_rate)*minV/maxV;
+	}
+
+	/* Acceleration. */
+	float a = (rate - pdata->last_rate)/elapsed_time_ms;
+	/* Remains this much. */
+	uint64_t r = estim->total_bytes - estim->current_byte;
+
+	int eta;
+	if(fabsf(a) >= 1 && rate*rate + 2*a*r >= 0)
+	{
+		eta = (sqrtf(rate*rate + 2*a*r) - rate)/a;
+	}
+	else if(fabsf(rate) >= 1)
+	{
+		eta = r/rate;
+	}
+	else
+	{
+		eta = pdata->last_eta;
+	}
+
+	/* Smooth ETA rate. */
+	window_add(&pdata->eta_window, eta);
+	float smooth_eta = window_average(&pdata->eta_window, &linear_weight);
+
 	pdata->last_calc_time = current_time_ms;
 	pdata->last_seen_byte = estim->current_byte;
+	pdata->last_rate = rate;
+	pdata->last_eta = smooth_eta;
+
+	format_io_stats(pdata, current_time_ms, imm_rate);
+}
+
+/* Adds an entry to window. */
+static void
+window_add(history_window_t *win, uint64_t value)
+{
+	win->entries[win->pos++] = value;
+	if(win->pos == ETA_HISTORY_SIZE)
+	{
+		win->pos = 0;
+		win->wrapped = 1;
+	}
+}
+
+/* Computes weighted average on entries of the passed in window.  Returns the
+ * average. */
+static float
+window_average(const history_window_t *win, weight_f weight)
+{
+	assert((win->pos != 0 || win->wrapped) && "Window must not be empty.");
+
+	float average = 0.0f;
+
+	/* Current weight. */
+	float w = 0.0f;
+	/* Total weight. */
+	float ws = 0.0f;
+
+	/* Start positions within circular buffer. */
+	int pos = (win->wrapped ? win->pos : 0);
+	/* Number of used positions. */
+	int count = (win->wrapped ? ETA_HISTORY_SIZE : win->pos);
+
+	int i;
+	for(i = 0; i < count; ++i)
+	{
+		w = weight(i, w);
+		ws += w;
+
+		average += w*win->entries[pos];
+		pos = (pos + 1)%ETA_HISTORY_SIZE;
+	}
+
+	average /= ws;
+	return average;
+}
+
+/* Linear weight function (1, 2, 3, ...).  Returns the next weight. */
+static float
+linear_weight(int idx, float last_weight)
+{
+	return 1 + idx;
+}
 
+/* Weight function with the factor of 2 (1, 2, 4, ...).  Returns the next
+ * weight. */
+static float
+doubling_weight(int idx, float last_weight)
+{
+	return (idx == 0 ? 1.0f : last_weight*2.0f);
+}
+
+/* Updates pdata->*_str fields. */
+static void
+format_io_stats(progress_data_t *pdata, long long now_ms, uint64_t imm_rate)
+{
 	char rate_str[64];
-	(void)friendly_size_notation(pdata->rate*1000, sizeof(rate_str) - 8,
-			rate_str);
+	(void)friendly_size_notation(imm_rate*1000, sizeof(rate_str) - 8, rate_str);
 	strcat(rate_str, "/s");
 	replace_string(&pdata->rate_str, rate_str);
+
+	/* Do not show ETA for the first 5 seconds.  Really short operations need no
+	 * ETA and long ones might have incorrect one at first. */
+	if(now_ms - pdata->start_time < 5000)
+	{
+		return;
+	}
+
+	enum
+	{
+		Minute = 60,
+		Hour = 60*Minute,
+		Day = 24*Hour,
+	};
+
+	int eta_s = ceil(pdata->last_eta/1000.0);
+	int d = eta_s/Day;
+	int h = eta_s%Day/Hour;
+	int m = eta_s%Hour/Minute;
+	int s = eta_s%Minute;
+
+	char *time_str;
+	if(d > 0)
+	{
+		time_str = format_str("%dd %02d:%02d:%02d", d, h, m, s);
+	}
+	else
+	{
+		time_str = format_str("%02d:%02d:%02d", h, m, s);
+	}
+	put_string(&pdata->eta_str, format_str("~%s left", time_str));
+	free(time_str);
 }
 
 /* Updates progress bar of operation. */
@@ -366,7 +532,7 @@ io_progress_fg(const io_progress_t *state, int progress)
 
 	item_num = MIN(estim->current_item + 1, estim->total_items);
 
-	update_io_rate(pdata, estim);
+	update_io_stats(pdata, estim);
 
 	if(progress < 0)
 	{
@@ -387,13 +553,14 @@ io_progress_fg(const io_progress_t *state, int progress)
 
 		draw_msgf(title, ctrl_msg, pdata->width,
 				"Location: %s\nItem:     %d of %" PRINTF_ULL "\n"
-				"Overall:  %s/%s (%2d%%) %s\n"
+				"Overall:  %5s/%-5s (%d%%)  |  %s  %s  %s\n"
 				"%s\n"
 				" \n" /* Space is on purpose to preserve empty line. */
 				"file %s\nfrom %s%s%s",
 				replace_home_part(ops->target_dir), item_num,
 				(unsigned long long)estim->total_items, current_size_str,
 				total_size_str, progress/IO_PRECISION, pdata->rate_str,
+				pdata->eta_str[0] == '\0' ? "" : "|", pdata->eta_str,
 				pdata->progress_bar, item_name, src_path, as_part, file_progress);
 
 		free(file_progress);
@@ -889,25 +1056,22 @@ edit_list(ext_edit_t *ext_edit, size_t orig_len, char *orig[], int *edited_len,
 	*edited_len = 0;
 
 	char rename_file[PATH_MAX + 1];
-	generate_tmp_file_name("vifm.rename", rename_file, sizeof(rename_file));
-
-	strlist_t prepared = ext_edit_prepare(ext_edit, orig, orig_len);
-
-	/* Allow temporary file to be only readable and writable by current user. */
-	mode_t saved_umask = umask(~0600);
-	const int write_error = (write_file_of_lines(rename_file, prepared.items,
-				prepared.nitems) != 0);
-	(void)umask(saved_umask);
-
-	free_string_array(prepared.items, prepared.nitems);
-
-	if(write_error)
+	FILE *file = make_file_in_tmp("vifm.rename", 0600, /*auto_delete=*/0,
+			rename_file, sizeof(rename_file));
+	if(file == NULL)
 	{
 		show_error_msgf("Error Getting List Of Renames",
 				"Can't create temporary file \"%s\": %s", rename_file, strerror(errno));
 		return NULL;
 	}
 
+	strlist_t prepared = ext_edit_prepare(ext_edit, orig, orig_len);
+
+	write_lines_to_file(file, prepared.items, prepared.nitems);
+	fclose(file);
+
+	free_string_array(prepared.items, prepared.nitems);
+
 	if(vim_view_file(rename_file, -1, -1, 0) != 0)
 	{
 		unlink(rename_file);
@@ -918,11 +1082,12 @@ edit_list(ext_edit_t *ext_edit, size_t orig_len, char *orig[], int *edited_len,
 
 	int result_len;
 	char **result = read_file_of_lines(rename_file, &result_len);
+	int error = errno;
 	unlink(rename_file);
 	if(result == NULL)
 	{
 		show_error_msgf("Error Getting List Of Renames",
-				"Can't open temporary file \"%s\": %s", rename_file, strerror(errno));
+				"Can't open temporary file \"%s\": %s", rename_file, strerror(error));
 		return NULL;
 	}
 
@@ -982,10 +1147,17 @@ alloc_progress_data(int bg, void *info)
 	pdata->progress_bar_max = 0;
 
 	/* Time of starting the operation to have meaningful first rate. */
-	pdata->last_calc_time = time_in_ms();
+	pdata->start_time = time_in_ms();
+	pdata->last_calc_time = pdata->start_time;
 	pdata->last_seen_byte = 0;
-	pdata->rate = 0;
+	pdata->last_rate = 0.0f;
+	pdata->last_eta = 0;
 	pdata->rate_str = strdup("? B/s");
+	pdata->eta_str = strdup("");
+	pdata->eta_window.wrapped = 0;
+	pdata->eta_window.pos = 0;
+	pdata->speed_window.wrapped = 0;
+	pdata->speed_window.pos = 0;
 
 	pdata->dialog = 0;
 	pdata->width = 0;
@@ -1006,6 +1178,12 @@ time_in_ms(void)
 	return current_time.tv_sec*1000 + current_time.tv_nsec/1000000;
 }
 
+int
+fops_active(const ops_t *ops)
+{
+	return !ops->aborted && !ui_cancellation_requested();
+}
+
 void
 fops_free_ops(ops_t *ops)
 {
@@ -1033,6 +1211,7 @@ fops_free_ops(ops_t *ops)
 		progress_data_t *pdata = ops->estim->param;
 		free(pdata->progress_bar);
 		free(pdata->rate_str);
+		free(pdata->eta_str);
 		free(pdata);
 	}
 	ops_free(ops);
diff --git a/src/fops_common.h b/src/fops_common.h
index 3622525..def6a92 100644
--- a/src/fops_common.h
+++ b/src/fops_common.h
@@ -57,11 +57,12 @@ typedef struct
 }
 bg_args_t;
 
+struct custom_prompt_t;
 struct dirent;
-struct response_variant;
 
-/* Callback for returning edited filename. */
-typedef void (*fo_prompt_cb)(const char new_filename[]);
+/* Callback for returning edited filename.  arg is user supplied value, which is
+ * passed through. */
+typedef void (*fo_prompt_cb)(const char new_filename[], void *arg);
 
 /* Line completion function.  arg is user supplied value, which is passed
  * through.  Should return completion offset. */
@@ -69,11 +70,10 @@ typedef int (*fo_complete_cmd_func)(const char cmd[], void *arg);
 
 /* Function to request filename editing. */
 typedef void (*line_prompt_func)(const char prompt[], const char filename[],
-		fo_prompt_cb cb, fo_complete_cmd_func complete, int allow_ee);
+		fo_prompt_cb cb, void *cb_arg, fo_complete_cmd_func complete, int allow_ee);
 
-/* Function to choose an option.  Returns choice. */
-typedef char (*options_prompt_func)(const char title[], const char message[],
-		const struct response_variant *variants);
+/* Function to choose an option.  Returns the choice. */
+typedef char (*options_prompt_func)(const struct custom_prompt_t *details);
 
 /* Function invoked to check whether edited list is OK.  Should return non-zero
  * if so and zero otherwise.  Should reallocate *error on error.  *data is the
@@ -166,6 +166,10 @@ void fops_bg_ops_init(ops_t *ops, bg_op_t *bg_op);
  * newly allocated structure, which should be freed by fops_free_ops(). */
 ops_t * fops_get_bg_ops(OPS main_op, const char descr[], const char dir[]);
 
+/* Checks whether operation should be carried on.  Returns zero if it was
+ * cancelled (via Ctrl-C) or aborted (via error dialog option) by the user. */
+int fops_active(const ops_t *ops);
+
 /* Frees ops structure previously obtained by call to get_ops().  ops can be
  * NULL. */
 void fops_free_ops(ops_t *ops);
diff --git a/src/fops_cpmv.c b/src/fops_cpmv.c
index 54dd58c..f871afa 100644
--- a/src/fops_cpmv.c
+++ b/src/fops_cpmv.c
@@ -215,60 +215,63 @@ fops_cpmv(view_t *view, char *list[], int nlines, CopyMoveLikeOp op, int flags)
 }
 
 void
-fops_replace(view_t *view, const char dst[], int force)
+fops_replace_entry(ops_t *ops, view_t *src, const dir_entry_t *src_entry,
+		view_t *dst, dir_entry_t *dst_entry)
 {
-	char undo_msg[2*PATH_MAX + 32];
-	dir_entry_t *entry;
-	char dst_dir[PATH_MAX + 1];
-	char src_full[PATH_MAX + 1];
-	const char *const fname = get_last_path_component(dst);
-	ops_t *ops;
 	void *cp = (void *)(size_t)1;
-	view_t *const other = (view == curr_view) ? other_view : curr_view;
+	int dst_exists = !fentry_is_fake(dst_entry);
+
+	char dst_dir[PATH_MAX + 1];
+	if(dst_exists)
+	{
+		copy_str(dst_dir, sizeof(dst_dir), dst_entry->origin);
+	}
+	else
+	{
+		const char *src_tail = src_entry->origin + strlen(flist_get_dir(src));
+		build_path(dst_dir, sizeof(dst_dir), flist_get_dir(dst), src_tail);
+	}
 
-	copy_str(dst_dir, sizeof(dst_dir), dst);
-	remove_last_path_component(dst_dir);
+	char dst_full[PATH_MAX + 1];
+	build_path(dst_full, sizeof(dst_full), dst_dir, src_entry->name);
 
-	entry = &view->dir_entry[view->list_pos];
-	get_full_path_of(entry, sizeof(src_full), src_full);
+	char src_full[PATH_MAX + 1];
+	get_full_path_of(src_entry, sizeof(src_full), src_full);
 
-	if(paths_are_same(src_full, dst))
+	if(paths_are_same(src_full, dst_full))
 	{
 		/* Nothing to do if destination and source are the same file. */
 		return;
 	}
 
-	ops = fops_get_ops(OP_COPY, "Copying", flist_get_dir(view), dst_dir);
-
-	snprintf(undo_msg, sizeof(undo_msg), "Copying %s to %s",
-			replace_home_part(src_full), dst_dir);
-
-	un_group_open(undo_msg);
-
-	if(path_exists(dst, NODEREF) && force)
+	/* Deleting it explicitly instead of letting cp_file() do it to move the file
+	 * to trash and make operation reversible. */
+	if(dst_exists && path_exists(dst_full, NODEREF))
 	{
-		(void)fops_delete_current(other, 1, 1);
+		fops_delete_entry(ops, dst, dst_entry, /*use_trash=*/1, /*nested=*/1);
 	}
 
 	fops_progress_msg("Copying files", 0, 1);
 
-	if(!ui_cancellation_requested() && !is_valid_dir(dst_dir) &&
-			perform_operation(OP_MKDIR, NULL, cp, dst_dir, NULL) == OPS_SUCCEEDED)
+	if(fops_active(ops) && !is_valid_dir(dst_dir) &&
+			perform_operation(OP_MKDIR, ops, cp, dst_dir, NULL) == OPS_SUCCEEDED)
 	{
 		un_group_add_op(OP_MKDIR, cp, NULL, dst_dir, "");
 	}
 
-	if(!ui_cancellation_requested())
+	if(fops_active(ops))
 	{
-		(void)cp_file(entry->origin, dst_dir, entry->name, fname, CMLO_COPY, 1, ops,
-				1);
+		/* Not forcing as destination path shouldn't exist. */
+		if(cp_file_f(src_full, dst_full, CMLO_COPY, /*bg=*/0, /*cancellable=*/1,
+					ops, /*force=*/0) == 0 && !dst_exists)
+		{
+			/* Update the destination entry to not be fake. */
+			replace_string(&dst_entry->name, src_entry->name);
+			replace_string(&dst_entry->origin, dst_dir);
+		}
 	}
 
-	un_group_close();
-
-	ui_view_schedule_reload(other);
-
-	fops_free_ops(ops);
+	ui_view_schedule_reload(dst);
 }
 
 /* Adapter for cp_file_f() that accepts paths broken into directory/file
diff --git a/src/fops_cpmv.h b/src/fops_cpmv.h
index 993ca95..03f0d11 100644
--- a/src/fops_cpmv.h
+++ b/src/fops_cpmv.h
@@ -20,6 +20,8 @@
 #ifndef VIFM__FOPS_CPMV_H__
 #define VIFM__FOPS_CPMV_H__
 
+struct dir_entry_t;
+struct ops_t;
 struct view_t;
 
 /* Type of copy/move-like operation. */
@@ -46,9 +48,11 @@ CopyMoveLikeFlags;
 int fops_cpmv(struct view_t *view, char *list[], int nlines, CopyMoveLikeOp op,
 		int flags);
 
-/* Replaces file specified by dst with a copy of the current file of the
- * view. */
-void fops_replace(struct view_t *view, const char dst[], int force);
+/* Replaces file entry of one view with another one.  Meant to be used for
+ * applying diff changes. */
+void fops_replace_entry(struct ops_t *ops, struct view_t *src,
+		const struct dir_entry_t *src_entry, struct view_t *dst,
+		struct dir_entry_t *dst_entry);
 
 /* Copies or moves marked files to the other view in background.  Flags is a
  * combination of CMLF_* values.  Returns new value for save_msg flag. */
diff --git a/src/fops_misc.c b/src/fops_misc.c
index 40ade43..7ed5801 100644
--- a/src/fops_misc.c
+++ b/src/fops_misc.c
@@ -71,7 +71,7 @@ static void delete_file_in_bg(ops_t *ops, const char path[], int use_trash);
 static int prepare_register(int reg);
 static char ** list_files_to_retarget(view_t *view, int *len);
 static int retarget_one(view_t *view);
-static void change_link_cb(const char new_target[]);
+static void change_link_cb(const char new_target[], void *arg);
 static int retarget_many(view_t *view, char *files[], int nfiles);
 static int verify_retarget_list(char *files[], int nfiles, char *names[],
 		int nnames, char **error, void *data);
@@ -88,13 +88,12 @@ static void go_to_first_file(view_t *view, char *names[], int count);
 static void update_dir_entry_size(dir_entry_t *entry, int force);
 static void start_dir_size_calc(const char path[], int force);
 static void dir_size_bg(bg_op_t *bg_op, void *arg);
-static void dir_size(bg_op_t *bg_op, char path[], int force);
+static void dir_size(bg_op_t *bg_op, const char path[], int force);
 static int bg_cancellation_hook(void *arg);
-static void redraw_after_path_change(view_t *view, const char path[]);
 #ifndef _WIN32
-static void change_owner_cb(const char new_owner[]);
+static void change_owner_cb(const char new_owner[], void *arg);
 static int complete_owner(const char str[], void *arg);
-static void change_group_cb(const char new_group[]);
+static void change_group_cb(const char new_group[], void *arg);
 static int complete_group(const char str[], void *arg);
 #endif
 
@@ -165,7 +164,7 @@ fops_delete(view_t *view, int reg, int use_trash)
 
 	entry = NULL;
 	i = 0;
-	while(iter_marked_entries(view, &entry) && !ui_cancellation_requested())
+	while(iter_marked_entries(view, &entry) && fops_active(ops))
 	{
 		int result;
 
@@ -199,52 +198,25 @@ fops_delete(view_t *view, int reg, int use_trash)
 	return 1;
 }
 
-int
-fops_delete_current(view_t *view, int use_trash, int nested)
+void
+fops_delete_entry(ops_t *ops, view_t *view, dir_entry_t *entry, int use_trash,
+		int nested)
 {
-	char undo_msg[COMMAND_GROUP_INFO_LEN];
-	dir_entry_t *entry;
-	ops_t *ops;
 	const char *const top_dir = get_top_dir(view);
-	const char *const curr_dir = top_dir == NULL ? flist_get_dir(view) : top_dir;
 
 	use_trash = use_trash && cfg.use_trash;
 
 	/* This check for the case when we are for sure in the trash. */
 	if(use_trash && top_dir != NULL && trash_has_path(top_dir))
 	{
-		show_error_msg("Can't perform deletion",
-				"Current directory is under trash directory");
-		return 0;
-	}
-
-	snprintf(undo_msg, sizeof(undo_msg), "%celete in %s: ", use_trash ? 'd' : 'D',
-			replace_home_part(curr_dir));
-	if(!nested)
-	{
-		un_group_open(undo_msg);
+		show_error_msgf("Can't delete to trash",
+				"Current directory is under trash directory:\n%s",
+				replace_home_part(top_dir));
+		return;
 	}
 
-	ops = fops_get_ops(OP_REMOVE, use_trash ? "deleting" : "Deleting", curr_dir,
-			curr_dir);
-
-	entry = &view->dir_entry[view->list_pos];
-
 	fops_progress_msg("Deleting files", 0, 1);
 	(void)delete_file(entry, ops, BLACKHOLE_REG_NAME, use_trash, nested);
-
-	if(!nested)
-	{
-		un_group_close();
-		ui_views_reload_filelists();
-	}
-
-	ui_sb_msgf("%d %s %celeted%s", ops->succeeded,
-			(ops->succeeded == 1) ? "file" : "files", use_trash ? 'd' : 'D',
-			fops_get_cancellation_suffix());
-
-	fops_free_ops(ops);
-	return 1;
 }
 
 /* Removes single file specified by its entry.  Returns zero on success,
@@ -463,6 +435,10 @@ delete_file_in_bg(ops_t *ops, const char path[], int use_trash)
 		return;
 	}
 
+	/* XXX: dealing with the trash unit from *background* thread here and in
+	 *      delete_files_in_bg() is likely to cause a race!
+	 *
+	 *      What to do?  Forbid bg trashing?  Do a snapshot to apply later? */
 	if(!trash_is_at_path(path))
 	{
 		const char *const fname = get_last_path_component(path);
@@ -617,14 +593,14 @@ retarget_one(view_t *view)
 		return 0;
 	}
 
-	fops_line_prompt("Link target: ", linkto, &change_link_cb, &complete_filename,
-			0);
+	fops_line_prompt("Link target: ", linkto, &change_link_cb, /*cb_arg=*/NULL,
+			&complete_filename, 0);
 	return 0;
 }
 
 /* Handles users response for new link target prompt. */
 static void
-change_link_cb(const char new_target[])
+change_link_cb(const char new_target[], void *arg)
 {
 	char undo_msg[2*PATH_MAX + 32];
 	char full_path[PATH_MAX + 1];
@@ -1320,7 +1296,7 @@ dir_size_bg(bg_op_t *bg_op, void *arg)
 /* Calculates directory size and triggers view updates if necessary.  Changes
  * path. */
 static void
-dir_size(bg_op_t *bg_op, char path[], int force)
+dir_size(bg_op_t *bg_op, const char path[], int force)
 {
 	const cancellation_t bg_cancellation_info = {
 		.arg = bg_op,
@@ -1329,10 +1305,10 @@ dir_size(bg_op_t *bg_op, char path[], int force)
 
 	(void)fops_dir_size(path, force, &bg_cancellation_info);
 
-	remove_last_path_component(path);
-
-	redraw_after_path_change(&lwin, path);
-	redraw_after_path_change(&rwin, path);
+	/* Redraw the views unconditionally, because checking their location from a
+	 * background thread will cause a data race. */
+	ui_view_schedule_redraw(&lwin);
+	ui_view_schedule_redraw(&rwin);
 }
 
 /* Implementation of cancellation hook for background tasks. */
@@ -1342,16 +1318,6 @@ bg_cancellation_hook(void *arg)
 	return bg_op_cancelled(arg);
 }
 
-/* Schedules view redraw in case path change might have affected it. */
-static void
-redraw_after_path_change(view_t *view, const char path[])
-{
-	if(path_starts_with(view->curr_dir, path) || flist_custom_active(view))
-	{
-		ui_view_schedule_redraw(view);
-	}
-}
-
 uint64_t
 fops_dir_size(const char path[], int force_update,
 		const cancellation_t *cancellation)
@@ -1495,12 +1461,13 @@ fops_chuser(void)
 		show_error_msg("Change owner", "No files to process");
 		return;
 	}
-	fops_line_prompt("New owner: ", "", &change_owner_cb, &complete_owner, 0);
+	fops_line_prompt("New owner: ", "", &change_owner_cb, /*cb_arg=*/NULL,
+			&complete_owner, 0);
 }
 
 /* Handles users response for new file owner name prompt. */
 static void
-change_owner_cb(const char new_owner[])
+change_owner_cb(const char new_owner[], void *arg)
 {
 	uid_t uid;
 
@@ -1536,12 +1503,13 @@ fops_chgroup(void)
 		show_error_msg("Change group", "No files to process");
 		return;
 	}
-	fops_line_prompt("New group: ", "", &change_group_cb, &complete_group, 0);
+	fops_line_prompt("New group: ", "", &change_group_cb, /*cb_arg=*/NULL,
+			&complete_group, 0);
 }
 
 /* Handles users response for new file group name prompt. */
 static void
-change_group_cb(const char new_group[])
+change_group_cb(const char new_group[], void *arg)
 {
 	gid_t gid;
 
diff --git a/src/fops_misc.h b/src/fops_misc.h
index e856d29..dd559ff 100644
--- a/src/fops_misc.h
+++ b/src/fops_misc.h
@@ -26,6 +26,8 @@
 
 #include "utils/test_helpers.h"
 
+struct dir_entry_t;
+struct ops_t;
 struct view_t;
 
 /* Removes marked files (optionally into trash directory) of the view to
@@ -33,10 +35,10 @@ struct view_t;
  * case status bar message is also printed. */
 int fops_delete(struct view_t *view, int reg, int use_trash);
 
-/* Removes current entry of the view.  Non-zero nested flag means that this is
- * not a standalone operation and is surrounded by other file operations.
- * Returns new value for save_msg flag. */
-int fops_delete_current(struct view_t *view, int use_trash, int nested);
+/* Removes an entry of the view.  Non-zero nested flag means that this is not a
+ * standalone operation and is surrounded by other file operations. */
+void fops_delete_entry(struct ops_t *ops, struct view_t *view,
+		struct dir_entry_t *entry, int use_trash, int nested);
 
 /* Initiates removal of marked files (optionally into trash directory) of the
  * view to specified register.  Returns new value for save_msg flag. */
diff --git a/src/fops_put.c b/src/fops_put.c
index b7e09e8..64257c4 100644
--- a/src/fops_put.c
+++ b/src/fops_put.c
@@ -21,6 +21,7 @@
 
 #include <assert.h> /* assert() */
 #include <ctype.h> /* tolower() */
+#include <limits.h> /* INT_MAX */
 #include <string.h> /* memmove() memset() strdup() */
 
 #include "cfg/config.h"
@@ -37,6 +38,7 @@
 #include "utils/str.h"
 #include "utils/string_array.h"
 #include "utils/test_helpers.h"
+#include "utils/utf8.h"
 #include "utils/utils.h"
 #include "background.h"
 #include "filelist.h"
@@ -48,6 +50,14 @@
 #include "trash.h"
 #include "undo.h"
 
+/* Information necessary for composing conflict message's body. */
+typedef struct
+{
+	const char *src; /* Full path of the source file. */
+	const char *dst; /* Full path of the destination file. */
+}
+conflict_prompt_data_t;
+
 static void put_files_in_bg(bg_op_t *bg_op, void *arg);
 static int initiate_put_files(view_t *view, int at, CopyMoveLikeOp op,
 		const char descr[], int reg_name);
@@ -66,17 +76,20 @@ static void prompt_what_to_do(const char fname[], const char caused_by[]);
 static void handle_prompt_response(const char fname[], const char caused_by[],
 		char response);
 static void prompt_dst_name(const char src_name[]);
-static void prompt_dst_name_cb(const char dst_name[]);
+static void prompt_dst_name_cb(const char dst_name[], void *arg);
 static void put_continue(int force);
-static void show_difference(const char fname[], const char caused_by[]);
-static char * compare_files(const char dst_path[], const char src_path[],
-		struct stat *dst, struct stat *src);
+static char * make_conflict_title(CopyMoveLikeOp op);
+static char * make_conflict_message(int max_w, int max_h, void *data);
+static char * make_conflict_prompt(const conflict_prompt_data_t *data,
+		int max_w, int *block_center);
+static char * prettify_fname(const char full_path[], const struct stat *stat);
+static char cmp_mark(int cmp);
 
 /* Global state for file putting and name conflicts resolution that happen in
  * the process. */
 static struct
 {
-	reg_t *reg;          /* Register used for the operation. */
+	const reg_t *reg;    /* Register used for the operation. */
 	int *file_order;     /* Defines custom ordering of files in register. */
 	view_t *view;        /* View in which operation takes place. */
 	CopyMoveLikeOp op;   /* Type of current operation. */
@@ -112,7 +125,6 @@ fops_put_bg(view_t *view, int at, int reg_name, int move)
 	size_t task_desc_len;
 	int i;
 	bg_args_t *args;
-	reg_t *reg;
 	const char *const dst_dir = fops_get_dst_dir(view, at);
 
 	/* Check that operation generally makes sense given our input. */
@@ -123,7 +135,7 @@ fops_put_bg(view_t *view, int at, int reg_name, int move)
 	}
 
 	regs_sync_from_shared_memory();
-	reg = regs_find(tolower(reg_name));
+	const reg_t *reg = regs_find(tolower(reg_name));
 	if(reg == NULL || reg->nfiles < 1)
 	{
 		ui_sb_err(reg == NULL ? "No such register" : "Register is empty");
@@ -272,7 +284,6 @@ static int
 initiate_put_files(view_t *view, int at, CopyMoveLikeOp op, const char descr[],
 		int reg_name)
 {
-	reg_t *reg;
 	int i;
 	const char *const dst_dir = fops_get_dst_dir(view, at);
 
@@ -282,7 +293,7 @@ initiate_put_files(view_t *view, int at, CopyMoveLikeOp op, const char descr[],
 	}
 
 	regs_sync_from_shared_memory();
-	reg = regs_find(tolower(reg_name));
+	const reg_t *reg = regs_find(tolower(reg_name));
 	if(reg == NULL || reg->nfiles < 1)
 	{
 		ui_sb_err("Register is empty");
@@ -413,7 +424,7 @@ is_dir_clash(const char src_path[], const char dst_dir[])
 {
 	char dst_path[PATH_MAX + 1];
 
-	snprintf(dst_path, sizeof(dst_path), "%s/%s", dst_dir,
+	build_path(dst_path, sizeof(dst_path), dst_dir,
 			fops_get_dst_name(src_path, trash_has_path(src_path)));
 	chosp(dst_path);
 
@@ -550,7 +561,7 @@ put_next(int force)
 	int move;
 	int success;
 	int merge;
-	int safe_operation = 0;
+	int clashing_op = 0;
 
 	/* TODO: refactor this function (put_next()) */
 
@@ -587,7 +598,7 @@ put_next(int force)
 		dst_name = fops_get_dst_name(src_buf, from_trash);
 	}
 
-	snprintf(dst_buf, sizeof(dst_buf), "%s/%s", dst_dir, dst_name);
+	build_path(dst_buf, sizeof(dst_buf), dst_dir, dst_name);
 	chosp(dst_buf);
 
 	if(!put_confirm.append && path_exists(dst_buf, NODEREF))
@@ -615,12 +626,12 @@ put_next(int force)
 					if(is_in_subtree(src_buf, dst_buf, 0))
 					{
 						/* Don't delete /a/b before moving /a/b/c to /a/b. */
-						safe_operation = 1;
+						clashing_op = 1;
 					}
 				}
 
-				if(!safe_operation && perform_operation(OP_REMOVESL, put_confirm.ops,
-							NULL, dst_buf, NULL) != OPS_SUCCEEDED)
+				if(!clashing_op && perform_operation(OP_REMOVESL, put_confirm.ops, NULL,
+							dst_buf, NULL) != OPS_SUCCEEDED)
 				{
 					show_error_msgf("Can't replace a file",
 							"Failed to remove a file:\n%s", dst_buf);
@@ -697,7 +708,7 @@ put_next(int force)
 
 		un_group_reopen_last();
 
-		snprintf(dst_path, sizeof(dst_path), "%s/%s", dst_dir, dst_name);
+		build_path(dst_path, sizeof(dst_path), dst_dir, dst_name);
 
 		if(merge_dirs(src_buf, dst_path, put_confirm.ops) != 0)
 		{
@@ -706,13 +717,16 @@ put_next(int force)
 
 		un_group_close();
 	}
-	else if(safe_operation)
+	else if(clashing_op)
 	{
+		/* We're replacing parent with its child.  To make this possible use a
+		 * temporary path next to the parent first, remove the parent, then move
+		 * data to where the parent was. */
 		const char *const unique_dst = make_name_unique(dst_buf);
 
-		/* An optimization: if we're going to remove destination anyway, don't
-		 * bother copying it, just move. */
-		if(op == OP_COPY)
+		/* An optimization: if we're going to remove original anyway, don't bother
+		 * copying it, just move. */
+		if(op == OP_COPY || op == OP_COPYF)
 		{
 			op = OP_MOVE;
 		}
@@ -779,7 +793,7 @@ put_next(int force)
 		}
 
 		char dst_path[PATH_MAX + 1];
-		snprintf(dst_path, sizeof(dst_path), "%s/%s", dst_dir, dst_name);
+		build_path(dst_path, sizeof(dst_path), dst_dir, dst_name);
 		put_confirm.put.nitems = add_to_string_array(&put_confirm.put.items,
 				put_confirm.put.nitems, dst_path);
 	}
@@ -846,8 +860,8 @@ merge_dirs(const char src[], const char dst[], ops_t *ops)
 			continue;
 		}
 
-		snprintf(src_path, sizeof(src_path), "%s/%s", src, d->d_name);
-		snprintf(dst_path, sizeof(dst_path), "%s/%s", dst, d->d_name);
+		build_path(src_path, sizeof(src_path), src, d->d_name);
+		build_path(dst_path, sizeof(dst_path), dst, d->d_name);
 
 		if(fops_is_dir_entry(dst_path, d))
 		{
@@ -918,7 +932,7 @@ handle_clashing(int move, const char src[], const char dst[])
 		int i;
 		char msg[PATH_MAX + 1];
 		response_variant responses[] = {
-			{ .key = 'y', .descr = "[y]es " },
+			{ .key = 'y', .descr = "[y]es /" },
 			{ .key = 'n', .descr = " [n]o\n" },
 			{ .key = NC_C_c, .descr = "\nEsc or Ctrl-C to abort" },
 			{}
@@ -930,7 +944,13 @@ handle_clashing(int move, const char src[], const char dst[])
 		snprintf(msg, sizeof(msg), "Overwriting\n%s\nwith\n%s\nwill result "
 				"in loss of the following files.  Are you sure?\n%s",
 				replace_home_part(dst), replace_home_part(src), vle_tb_get_data(lost));
-		switch(fops_options_prompt("Possible data loss", msg, responses))
+
+		const custom_prompt_t prompt = {
+			.title = "Possible data loss",
+			.message = msg,
+			.variants = responses,
+		};
+		switch(fops_options_prompt(&prompt))
 		{
 			case 'y':
 				/* Do nothing. */
@@ -988,33 +1008,26 @@ prompt_what_to_do(const char fname[], const char caused_by[])
 	/* Strange spacing is for left alignment.  Doesn't look nice here, but it is
 	 * problematic to get such alignment otherwise. */
 	static const response_variant
-		compare        = { .key = 'c', .descr = "[c]ompare files              \n" },
-		rename         = { .key = 'r', .descr = "[r]ename (also Enter)        \n" },
+		rename         = { .key = 'r', .descr = "[r]ename (also Enter)\n" },
 		enter          = { .key = '\r', .descr = "" },
-		skip           = { .key = 's', .descr = "[s]kip " },
-		skip_all       = { .key = 'S', .descr = " [S]kip all          \n" },
-		append         = { .key = 'a', .descr = "[a]ppend the tail            \n" },
-		overwrite      = { .key = 'o', .descr = "[o]verwrite " },
+		skip           = { .key = 's', .descr = "[s]kip /" },
+		skip_all       = { .key = 'S', .descr = " [S]kip all\n" },
+		append         = { .key = 'a', .descr = "[a]ppend the tail\n" },
+		overwrite      = { .key = 'o', .descr = "[o]verwrite /" },
 		overwrite_all  = { .key = 'O', .descr = " [O]verwrite all\n" },
-		merge          = { .key = 'm', .descr = "[m]erge " },
-		merge_all      = { .key = 'M', .descr = " [M]erge all        \n" },
-		merge_all_only = { .key = 'M', .descr = "[M]erge all                  \n" },
-		escape         = { .key = NC_C_c, .descr = "\nEsc or Ctrl-C to cancel" };
+		merge          = { .key = 'm', .descr = "[m]erge /" },
+		merge_all      = { .key = 'M', .descr = " [M]erge all\n" },
+		merge_all_only = { .key = 'M', .descr = "[M]erge all\n" },
+		escape         = { .key = NC_C_c, .descr = "\n   Esc or Ctrl-C to abort" };
 
-	char response;
 	/* Last element is a terminator. */
 	response_variant responses[12] = {};
 	size_t i = 0;
 
 	char dst_buf[PATH_MAX + 1];
-	snprintf(dst_buf, sizeof(dst_buf), "%s/%s", put_confirm.dst_dir, fname);
+	build_path(dst_buf, sizeof(dst_buf), put_confirm.dst_dir, fname);
 	const int same_file = paths_are_equal(dst_buf, caused_by);
 
-	if(!same_file)
-	{
-		responses[i++] = compare;
-	}
-
 	responses[i++] = rename;
 	responses[i++] = enter;
 
@@ -1023,7 +1036,7 @@ prompt_what_to_do(const char fname[], const char caused_by[])
 
 	if(!same_file)
 	{
-		if(cfg.use_system_calls && is_regular_file_noderef(fname) &&
+		if(cfg.use_system_calls && is_regular_file_noderef(dst_buf) &&
 				is_regular_file_noderef(caused_by))
 		{
 			responses[i++] = append;
@@ -1046,27 +1059,32 @@ prompt_what_to_do(const char fname[], const char caused_by[])
 	/* Screen needs to be restored after displaying progress dialog. */
 	modes_update();
 
-	char *escaped_cause = escape_unreadable(replace_home_part(caused_by));
-
-	char msg[PATH_MAX*3];
-	if(same_file)
-	{
-		snprintf(msg, sizeof(msg),
-				"Same file is both source and destination:\n%s\nWhat to do?",
-				escaped_cause);
-	}
-	else
-	{
-		char *escaped_fname = escape_unreadable(fname);
-		snprintf(msg, sizeof(msg),
-				"Name conflict for %s.  Caused by:\n%s\nWhat to do?", escaped_fname,
-				escaped_cause);
-		free(escaped_fname);
+	/* Invoking make_conflict_prompt() to get block_center flag and validate the
+	 * operation (we're not showing the prompt if NULL is returned). */
+	conflict_prompt_data_t prompt_data = {
+		.src = caused_by,
+		.dst = dst_buf,
+	};
+	int block_center;
+	char *msg =
+		make_conflict_prompt(&prompt_data, /*max_w=*/INT_MAX, &block_center);
+	free(msg);
+
+	char response = NC_C_c;
+	char *title = make_conflict_title(put_confirm.op);
+	if(title != NULL && msg != NULL)
+	{
+		const custom_prompt_t prompt = {
+			.title = title,
+			.make_message = &make_conflict_message,
+			.user_data = &prompt_data,
+			.variants = responses,
+			.block_center = block_center,
+		};
+		response = fops_options_prompt(&prompt);
 	}
+	free(title);
 
-	free(escaped_cause);
-
-	response = fops_options_prompt("File Conflict", msg, responses);
 	handle_prompt_response(fname, caused_by, response);
 }
 
@@ -1076,7 +1094,7 @@ handle_prompt_response(const char fname[], const char caused_by[],
 		char response)
 {
 	char dst_path[PATH_MAX + 1];
-	snprintf(dst_path, sizeof(dst_path), "%s/%s", put_confirm.dst_dir, fname);
+	build_path(dst_path, sizeof(dst_path), put_confirm.dst_dir, fname);
 
 	/* Record last conflict to position cursor at it later. */
 	update_string(&put_confirm.last_conflict, dst_path);
@@ -1085,11 +1103,6 @@ handle_prompt_response(const char fname[], const char caused_by[],
 	{
 		prompt_dst_name(fname);
 	}
-	else if(response == 'c')
-	{
-		show_difference(fname, caused_by);
-		prompt_what_to_do(fname, caused_by);
-	}
 	else if(response == 's' || response == 'S')
 	{
 		if(response == 'S')
@@ -1148,12 +1161,13 @@ prompt_dst_name(const char src_name[])
 	char prompt[128 + PATH_MAX];
 
 	snprintf(prompt, ARRAY_LEN(prompt), "New name for %s: ", src_name);
-	fops_line_prompt(prompt, src_name, &prompt_dst_name_cb, NULL, 0);
+	fops_line_prompt(prompt, src_name, &prompt_dst_name_cb, /*cb_arg=*/NULL, NULL,
+			0);
 }
 
 /* Callback for line prompt result. */
 static void
-prompt_dst_name_cb(const char dst_name[])
+prompt_dst_name_cb(const char dst_name[], void *arg)
 {
 	if(is_null_or_empty(dst_name))
 	{
@@ -1162,7 +1176,7 @@ prompt_dst_name_cb(const char dst_name[])
 
 	/* Record new destination path. */
 	char dst_path[PATH_MAX + 1];
-	snprintf(dst_path, sizeof(dst_path), "%s/%s", put_confirm.dst_dir, dst_name);
+	build_path(dst_path, sizeof(dst_path), put_confirm.dst_dir, dst_name);
 	update_string(&put_confirm.last_conflict, dst_path);
 
 	if(replace_string(&put_confirm.dst_name, dst_name) != 0)
@@ -1189,88 +1203,156 @@ put_continue(int force)
 	}
 }
 
-/* Displays differences in metadata among two conflicting files in a dialog. */
-static void
-show_difference(const char fname[], const char caused_by[])
+/* Formats a title for conflict message.  Returns newly allocated string. */
+static char *
+make_conflict_title(CopyMoveLikeOp op)
 {
-	char dst_path[PATH_MAX + 1];
-	snprintf(dst_path, sizeof(dst_path), "%s/%s", put_confirm.dst_dir, fname);
-
-	struct stat dst;
-	if(os_stat(dst_path, &dst) != 0)
-	{
-		show_error_msgf("Comparison error", "Unable to query metadata of %s",
-				dst_path);
-		return;
-	}
-
-	struct stat src;
-	if(os_stat(caused_by, &src) != 0)
+	const char *action = "?";
+	switch(op)
 	{
-		show_error_msgf("Comparison error", "Unable to query metadata of %s",
-				caused_by);
-		return;
+		case CMLO_COPY:
+			action = "Copying";
+			break;
+		case CMLO_MOVE:
+			action = "Moving";
+			break;
+		case CMLO_LINK_REL:
+		case CMLO_LINK_ABS:
+			action = "Symlinking";
+			break;
 	}
 
-	char *diff = compare_files(dst_path, caused_by, &dst, &src);
-
-	static const response_variant responses[] = {
-		{ .key = '\r', .descr = "Press Enter to continue", },
-		{ },
-	};
-	(void)prompt_msg_custom("File difference", diff, responses);
-
-	free(diff);
+	return format_str("File Conflict on %s", action);
 }
 
-/* Produces textual description of metadata difference between two files.
- * Returns newly allocated string. */
+/* Adapts make_conflict_prompt() for use with fops_options_prompt().  Returns
+ * newly allocated string. */
 static char *
-compare_files(const char dst_path[], const char src_path[], struct stat *dst,
-		struct stat *src)
+make_conflict_message(int max_w, int max_h, void *data)
 {
-	vle_textbuf *text = vle_tb_create();
-
-	vle_tb_append_linef(text, "Target file: %s", replace_home_part(dst_path));
-	vle_tb_append_linef(text, "Source file: %s", replace_home_part(src_path));
+	int block_center;
+	return make_conflict_prompt(data, max_w, &block_center);
+}
 
-	char buf[64];
+/* Produces text for a conflict prompt.  Returns newly allocated string. */
+static char *
+make_conflict_prompt(const conflict_prompt_data_t *data, int max_w,
+		int *block_center)
+{
+	*block_center = 1;
 
-	vle_tb_append_line(text, " ");
-	format_iso_time(dst->st_mtime, buf, sizeof(buf));
-	if(dst->st_mtime == src->st_mtime)
+	struct stat src;
+	if(os_lstat(data->src, &src) != 0)
 	{
-		vle_tb_append_linef(text, "Same modification date: %s", buf);
+		show_error_msgf("Conflict handling error", "Unable to query metadata of %s",
+				data->src);
+		return NULL;
 	}
-	else
+
+	char *pretty_src = prettify_fname(data->src, &src);
+	if(pretty_src == NULL)
 	{
-		vle_tb_append_line(text, "Modification dates:");
-		vle_tb_append_linef(text, "%s", buf);
+		return NULL;
+	}
 
-		format_iso_time(src->st_mtime, buf, sizeof(buf));
-		vle_tb_append_linef(text, "%s", buf);
+	if(paths_are_equal(data->src, data->dst))
+	{
+		*block_center = 0;
+		char *text =
+			format_str("Same file is both source and destination:\n%s", pretty_src);
+		free(pretty_src);
+		return text;
 	}
 
-	vle_tb_append_line(text, " ");
-	(void)friendly_size_notation(dst->st_size, sizeof(buf), buf);
-	if(dst->st_size == src->st_size)
+	struct stat dst;
+	if(os_lstat(data->dst, &dst) != 0)
 	{
-		vle_tb_append_linef(text, "Same size: %s (%llu)", buf,
-				(unsigned long long)dst->st_size);
+		show_error_msgf("Conflict handling error", "Unable to query metadata of %s",
+				data->dst);
+		free(pretty_src);
+		return NULL;
 	}
-	else
+
+	char *pretty_dst = prettify_fname(data->dst, &dst);
+	if(pretty_dst == NULL)
 	{
-		vle_tb_append_line(text, "Sizes:");
-		vle_tb_append_linef(text, "%s (%llu)", buf,
-				(unsigned long long)dst->st_size);
+		free(pretty_src);
+		return NULL;
+	}
+
+	char src_size[64], dst_size[64];
+	(void)friendly_size_notation(src.st_size, sizeof(src_size), src_size);
+	(void)friendly_size_notation(dst.st_size, sizeof(dst_size), dst_size);
+
+	char src_bsize[64], dst_bsize[64];
+	snprintf(src_bsize, sizeof(src_bsize), " (%" PRINTF_ULL ")",
+			(unsigned long long)src.st_size);
+	snprintf(dst_bsize, sizeof(dst_bsize), " (%" PRINTF_ULL ")",
+			(unsigned long long)dst.st_size);
 
-		(void)friendly_size_notation(src->st_size, sizeof(buf), buf);
-		vle_tb_append_linef(text, "%s (%llu)", buf,
-				(unsigned long long)src->st_size);
+	char src_mtime[64], dst_mtime[64];
+	format_iso_time(src.st_mtime, src_mtime, sizeof(src_mtime));
+	format_iso_time(dst.st_mtime, dst_mtime, sizeof(dst_mtime));
+
+	int fields[3] = { };
+	fields[0] = MAX(strlen(src_size), strlen(dst_size));
+	fields[1] = MAX(strlen(src_bsize), strlen(dst_bsize));
+	fields[2] = MAX(utf8_strsw(src_mtime), utf8_strsw(dst_mtime));
+
+	const char *info_fmt = "      %c %*s%*s   %c %*s";
+	int l = snprintf(NULL, 0, info_fmt,
+			'x', fields[0], "", fields[1], "",
+			'y', fields[2], "");
+	if(l > max_w)
+	{
+		info_fmt = "      %c %*s%*s\n      %c %*s";
 	}
 
+#define CMP(a, b) ((a) == (b) ? 0 : (a) < (b) ? -1 : 1)
+	int size_cmp = CMP(src.st_size, dst.st_size);
+	int mtime_cmp = CMP(src.st_mtime, dst.st_mtime);
+#undef CMP
+
+	vle_textbuf *text = vle_tb_create();
+
+	vle_tb_append_linef(text, "From: %s", pretty_src);
+	vle_tb_append_linef(text, info_fmt,
+			cmp_mark(size_cmp), fields[0], src_size, fields[1], src_bsize,
+			cmp_mark(mtime_cmp), fields[2], src_mtime);
+
+	vle_tb_append_line(text, " ");
+
+	vle_tb_append_linef(text, "  To: %s", pretty_dst);
+	vle_tb_append_linef(text, info_fmt,
+			cmp_mark(-size_cmp), fields[0], dst_size, fields[1], dst_bsize,
+			cmp_mark(-mtime_cmp), fields[2], dst_mtime);
+
+	free(pretty_src);
+	free(pretty_dst);
+
 	return vle_tb_release(text);
 }
 
+/* Prepares file name for being displayed in a dialog.  Returns newly allocated
+ * string. */
+static char *
+prettify_fname(const char full_path[], const struct stat *stat)
+{
+	char *pretty = escape_unreadable(replace_home_part(full_path));
+	if(pretty != NULL && S_ISDIR(stat->st_mode))
+	{
+		put_string(&pretty, format_str("%s/", pretty));
+	}
+	return pretty;
+}
+
+/* Translates comparison result to one of three marks: <, =, >.  Returns the
+ * mark. */
+static char
+cmp_mark(int cmp)
+{
+	return (cmp == 0 ? '=' : cmp < 0 ? '<' : '>');
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 : */
diff --git a/src/fops_rename.c b/src/fops_rename.c
index 2c68bb4..82002a2 100644
--- a/src/fops_rename.c
+++ b/src/fops_rename.c
@@ -48,7 +48,7 @@ typedef enum
 }
 RenameAction;
 
-static void rename_file_cb(const char new_name[]);
+static void rename_file_cb(const char new_name[], void *arg);
 static int complete_filename_only(const char str[], void *arg);
 static char ** list_files_to_rename(view_t *view, int recursive, int *len);
 static int verify_list(char *files[], int nfiles, char *names[], int nnames,
@@ -66,12 +66,13 @@ static RenameAction check_rename(const char old_fname[], const char new_fname[],
 static int rename_marked(view_t *view, const char desc[], const char lhs[],
 		const char rhs[], char **dest);
 
-/* Temporary storage for extension of file being renamed in name-only mode. */
-static char rename_file_ext[NAME_MAX + 1];
-
 void
 fops_rename_current(view_t *view, int name_only)
 {
+	/* Temporary storage for extension of file being renamed in name-only mode.
+	 * There is no nesting of renames, so single static buffer is enough. */
+	static char rename_file_ext[NAME_MAX + 1];
+
 	const dir_entry_t *const curr = get_current_entry(view);
 	char filename[strlen(curr->name) + 1];
 
@@ -102,13 +103,20 @@ fops_rename_current(view_t *view, int name_only)
 
 	flist_sel_stash(view);
 	fops_line_prompt(name_only ? "New name root: " : "New full name: ", filename,
-			&rename_file_cb, &complete_filename_only, 1);
+			&rename_file_cb, rename_file_ext, &complete_filename_only, 1);
 }
 
 /* Callback for processing file rename query. */
 static void
-rename_file_cb(const char new_name[])
+rename_file_cb(const char new_name[], void *arg)
 {
+	if(is_null_or_empty(new_name))
+	{
+		return;
+	}
+
+	const char *rename_file_ext = arg;
+
 	char buf[MAX(COMMAND_GROUP_INFO_LEN, 10 + NAME_MAX + 1)];
 	char new[strlen(new_name) + 1 + strlen(rename_file_ext) + 1 + 1];
 	int mv_res;
@@ -116,11 +124,6 @@ rename_file_cb(const char new_name[])
 	const char *const fname = curr->name;
 	const char *const forigin = curr->origin;
 
-	if(is_null_or_empty(new_name))
-	{
-		return;
-	}
-
 	if(contains_slash(new_name))
 	{
 		ui_sb_err("Name can not contain slash");
diff --git a/src/instance.c b/src/instance.c
index eb564b7..f0ddb0d 100644
--- a/src/instance.c
+++ b/src/instance.c
@@ -39,6 +39,7 @@
 #include "plugins.h"
 #include "registers.h"
 #include "status.h"
+#include "trash.h"
 #include "undo.h"
 #include "vifm.h"
 
@@ -131,12 +132,19 @@ instance_finish_restart(void)
 	cs_load_pairs();
 
 	cfg_load();
-	plugs_load(curr_stats.plugs, cfg.config_dir);
+	plugs_load(curr_stats.plugs, curr_stats.plugins_dirs);
 
 	vifm_reexec_startup_commands();
 
 	curr_stats.restart_in_progress = 0;
 
+	if(cfg.use_trash)
+	{
+		/* Applying 'trashdir' is postponed during restart to not create directory
+		 * at default location. */
+		(void)trash_set_specs(cfg.trash_dir);
+	}
+
 	/* Trigger auto-commands for initial directories. */
 	vle_aucmd_execute("DirEnter", flist_get_dir(&lwin), &lwin);
 	vle_aucmd_execute("DirEnter", flist_get_dir(&rwin), &rwin);
diff --git a/src/int/file_magic.c b/src/int/file_magic.c
index 61c97c7..6c3e837 100644
--- a/src/int/file_magic.c
+++ b/src/int/file_magic.c
@@ -40,6 +40,7 @@
 #include "../utils/fsddata.h"
 #include "../utils/path.h"
 #include "../utils/str.h"
+#include "../utils/utils.h"
 #include "../filetype.h"
 #include "../status.h"
 #include "desktop.h"
@@ -255,7 +256,8 @@ get_file_mimetype(const char filename[], char buf[], size_t buf_sz)
 #ifdef HAVE_FILE_PROG
 	FILE *pipe;
 	char command[1024];
-	char *const escaped_filename = shell_like_escape(filename, 0);
+	ShellType shell_type = (get_env_type() == ET_UNIX ? ST_POSIX : ST_CMD);
+	char *const escaped_filename = shell_arg_escape(filename, shell_type);
 
 	/* Use the file command to get mimetype */
 	snprintf(command, sizeof(command), "file %s -b --mime-type",
diff --git a/src/int/fuse.c b/src/int/fuse.c
index b76e1da..4187182 100644
--- a/src/int/fuse.c
+++ b/src/int/fuse.c
@@ -21,18 +21,18 @@
 
 #include <curses.h> /* werase() */
 
-#include <sys/types.h> /* pid_t ssize_t */
+#include <sys/types.h> /* pid_t */
 #include <sys/stat.h> /* S_IRWXU */
 #ifndef _WIN32
-#include <sys/wait.h> /* WEXITSTATUS() WIFEXITED() waitpid() */
+#include <sys/wait.h> /* WEXITSTATUS() WIFEXITED() */
 #endif
-#include <unistd.h> /* execve() fork() unlink() */
+#include <unistd.h> /* unlink() */
 
 #include <errno.h> /* errno ENOTDIR */
 #include <stddef.h> /* NULL size_t */
 #include <stdio.h> /* snprintf() fclose() */
 #include <stdlib.h> /* EXIT_SUCCESS free() malloc() */
-#include <string.h> /* memmove() strcpy() strlen() strcmp() strcat() */
+#include <string.h> /* memmove() strcpy() strlen() strcmp() */
 
 #include "../cfg/config.h"
 #include "../compat/fs_limits.h"
@@ -43,6 +43,7 @@
 #include "../ui/statusbar.h"
 #include "../ui/ui.h"
 #include "../utils/cancellation.h"
+#include "../utils/file_streams.h"
 #include "../utils/fs.h"
 #include "../utils/log.h"
 #include "../utils/macros.h"
@@ -78,8 +79,9 @@ TSTATIC void format_mount_command(const char mount_point[],
 static fuse_mount_t * get_mount_by_source(const char source[]);
 static fuse_mount_t * get_mount_by_mount_point(const char dir[]);
 static fuse_mount_t * get_mount_by_path(const char path[]);
-static int run_fuse_command(char cmd[], const cancellation_t *cancellation,
-		int *cancelled);
+static int run_fuse_command(char cmd[], int *cancelled, char **errors);
+static char * read_proc_stream(pid_t pid, FILE *fp,
+		const cancellation_t *cancellation);
 static void kill_mount_point(const char mount_point[]);
 static void updir_from_mount(view_t *view, fuse_mount_t *runner);
 
@@ -178,11 +180,7 @@ fuse_mount(view_t *view, char file_full_path[], const char param[],
 
 	int id;
 	int mount_point_id;
-	char buf[2*PATH_MAX];
 	int foreground;
-	char errors_file[PATH_MAX + 1];
-	int status;
-	int cancelled;
 
 	id = get_last_mount_point_id(fuse_mounts);
 	mount_point_id = id;
@@ -220,8 +218,9 @@ fuse_mount(view_t *view, char file_full_path[], const char param[],
 		return -1;
 	}
 
-	format_mount_command(mount_point, file_full_path, param, program, sizeof(buf),
-			buf, &foreground);
+	char mount_cmd[2*PATH_MAX];
+	format_mount_command(mount_point, file_full_path, param, program,
+			sizeof(mount_cmd), mount_cmd, &foreground);
 
 	ui_sb_msg("FUSE mounting selected file, please stand by..");
 
@@ -230,30 +229,17 @@ fuse_mount(view_t *view, char file_full_path[], const char param[],
 		ui_shutdown();
 	}
 
-	generate_tmp_file_name("vifm.errors", errors_file, sizeof(errors_file));
-
-	strcat(buf, " 2> ");
-	strcat(buf, errors_file);
-	LOG_INFO_MSG("FUSE mount command: `%s`", buf);
-	if(foreground)
-	{
-		cancelled = 0;
-		status = run_fuse_command(buf, &no_cancellation, NULL);
-	}
-	else
-	{
-		ui_cancellation_push_on();
-		status = run_fuse_command(buf, &ui_cancellation_info, &cancelled);
-		ui_cancellation_pop();
-	}
+	LOG_INFO_MSG("FUSE mount command: `%s`", mount_cmd);
+	int cancelled = 0;
+	char *mounter_errors = NULL;
+	int status = run_fuse_command(mount_cmd, foreground ? NULL : &cancelled,
+			&mounter_errors);
 
 	ui_sb_clear();
 
 	/* Check child process exit status. */
 	if(!WIFEXITED(status) || WEXITSTATUS(status) != EXIT_SUCCESS)
 	{
-		FILE *ef;
-
 		if(!WIFEXITED(status))
 		{
 			LOG_ERROR_MSG("FUSE mounter didn't exit!");
@@ -263,13 +249,11 @@ fuse_mount(view_t *view, char file_full_path[], const char param[],
 			LOG_ERROR_MSG("FUSE mount command exit status: %d", WEXITSTATUS(status));
 		}
 
-		ef = os_fopen(errors_file, "r");
-		if(ef == NULL)
+		if(!is_null_or_empty(mounter_errors))
 		{
-			LOG_SERROR_MSG(errno, "Failed to open temporary stderr file: %s",
-					errors_file);
+			show_error_msg("FUSE Mounter Errors", mounter_errors);
 		}
-		show_errors_from_file(ef, "FUSE mounter error");
+		free(mounter_errors);
 
 		werase(status_bar);
 
@@ -280,12 +264,7 @@ fuse_mount(view_t *view, char file_full_path[], const char param[],
 		}
 		else
 		{
-			show_error_msg("FUSE MOUNT ERROR", file_full_path);
-		}
-
-		if(unlink(errors_file) != 0)
-		{
-			LOG_SERROR_MSG(errno, "Error file deletion failure: %s", errors_file);
+			show_error_msgf("FUSE", "Failed to mount file: %s", file_full_path);
 		}
 
 		/* Remove the directory we created for the mount. */
@@ -294,7 +273,6 @@ fuse_mount(view_t *view, char file_full_path[], const char param[],
 		(void)vifm_chdir(flist_get_dir(view));
 		return -1;
 	}
-	unlink(errors_file);
 	ui_sb_msg("FUSE mount success");
 
 	register_mount(&fuse_mounts, file_full_path, mount_point, mount_point_id,
@@ -360,8 +338,8 @@ format_mount_command(const char mount_point[], const char file_name[],
 
 	*foreground = 0;
 
-	escaped_path = shell_like_escape(file_name, 0);
-	escaped_mount_point = shell_like_escape(mount_point, 0);
+	escaped_path = shell_arg_escape(file_name, curr_stats.shell_type);
+	escaped_mount_point = shell_arg_escape(mount_point, curr_stats.shell_type);
 
 	buf_pos = buf;
 	buf_pos[0] = '\0';
@@ -442,7 +420,8 @@ fuse_unmount_all(void)
 	{
 		if(runner->needs_unmounting)
 		{
-			char *escaped_filename = shell_like_escape(runner->mount_point, 0);
+			char *escaped_filename =
+				shell_arg_escape(runner->mount_point, curr_stats.shell_type);
 			char buf[14 + PATH_MAX + 1];
 			snprintf(buf, sizeof(buf), "%s %s", curr_stats.fuse_umount_cmd,
 					escaped_filename);
@@ -555,12 +534,13 @@ fuse_try_unmount(view_t *view)
 
 	if(runner->needs_unmounting)
 	{
-		char *escaped_mount_point = shell_like_escape(runner->mount_point, 0);
+		char *escaped_mount_point =
+			shell_arg_escape(runner->mount_point, curr_stats.shell_type);
 
-		char buf[14 + PATH_MAX + 1];
-		snprintf(buf, sizeof(buf), "%s %s 2> /dev/null", curr_stats.fuse_umount_cmd,
-				escaped_mount_point);
-		LOG_INFO_MSG("FUSE unmount command: `%s`", buf);
+		char unmount_cmd[14 + PATH_MAX + 1];
+		snprintf(unmount_cmd, sizeof(unmount_cmd), "%s %s",
+				curr_stats.fuse_umount_cmd, escaped_mount_point);
+		LOG_INFO_MSG("FUSE unmount command: `%s`", unmount_cmd);
 		free(escaped_mount_point);
 
 		/* Have to chdir to parent temporarily, so that this DIR can be
@@ -571,8 +551,9 @@ fuse_try_unmount(view_t *view)
 			return -1;
 		}
 
-		ui_sb_msg("FUSE unmounting selected file, please stand by..");
-		int status = run_fuse_command(buf, &no_cancellation, NULL);
+		ui_sb_msg("FUSE unmounting selected file, please stand by...");
+		int status = run_fuse_command(unmount_cmd, /*cancelled=*/NULL,
+				/*errors=*/NULL);
 		ui_sb_clear();
 		/* Check child status. */
 		if(!WIFEXITED(status) || WEXITSTATUS(status))
@@ -600,65 +581,82 @@ fuse_try_unmount(view_t *view)
 	return 1;
 }
 
-/* Runs command in background not redirecting its streams.  To determine an
- * error uses exit status only.  cancelled can be NULL when operations is not
- * cancellable.  Returns status on success, otherwise -1 is returned.  Sets
- * correct value of *cancelled even on error. */
+/* Runs command in background keeping its stdin and stdout connected to the
+ * terminal.  NULL in the cancelled parameter means that the operations is not
+ * cancellable, otherwise *cancelled is set to the correct value even on error.
+ * If the errors parameter isn't NULL, stderr output is returned, otherwise
+ * it's captured and discarded.  Returns status on success, otherwise -1 is
+ * returned. */
 static int
-run_fuse_command(char cmd[], const cancellation_t *cancellation, int *cancelled)
+run_fuse_command(char cmd[], int *cancelled, char **errors)
 {
-#ifndef _WIN32
-	pid_t pid;
-	int status;
-
-	if(cancellation_possible(cancellation))
+	const cancellation_t *cancellation = &no_cancellation;
+	if(cancelled != NULL)
 	{
+		cancellation = &ui_cancellation_info;
 		*cancelled = 0;
 	}
 
-	if(cmd == NULL)
-	{
-		return 1;
-	}
-
-	pid = fork();
+	FILE *err;
+	pid_t pid =
+		bg_run_and_capture(cmd, /*user_sh=*/0, /*in=*/NULL, /*out=*/NULL, &err);
 	if(pid == (pid_t)-1)
 	{
-		LOG_SERROR_MSG(errno, "Forking has failed.");
 		return -1;
 	}
 
-	if(pid == (pid_t)0)
+	if(cancelled != NULL)
 	{
-		extern char **environ;
-
-		prepare_for_exec();
-		(void)execve(get_execv_path(cfg.shell),
-				make_execv_array(cfg.shell, "-c", cmd), environ);
-		_Exit(127);
+		ui_cancellation_push_on();
 	}
 
-	while(waitpid(pid, &status, 0) == -1)
+	/* We're always reading the error stream, but dropping all that we've read
+	 * unless errors parameter is non-NULL. */
+	char *errors_buf = read_proc_stream(pid, err, cancellation);
+	fclose(err);
+
+	if(errors != NULL)
 	{
-		if(errno != EINTR)
-		{
-			LOG_SERROR_MSG(errno, "Failed waiting for process: %" PRINTF_ULL,
-					(unsigned long long)pid);
-			status = -1;
-			break;
-		}
-		process_cancel_request(pid, cancellation);
+		*errors = errors_buf;
+		errors_buf = NULL;
 	}
+	free(errors_buf);
 
-	if(cancellation_requested(cancellation))
+	/* The only bit that can't compile on Windows.  Also mind that
+	 * bg_run_and_capture() doesn't actually return PID on Windows. */
+#ifndef _WIN32
+	int status = get_proc_exit_status(pid, cancellation);
+#else
+	int status = -1;
+#endif
+
+	if(cancelled != NULL)
 	{
-		*cancelled = 1;
+		*cancelled = cancellation_requested(cancellation);
+		ui_cancellation_pop();
 	}
 
 	return status;
-#else
-	return -1;
-#endif
+}
+
+/* Reads redirected stream from the process while handling cancellation.
+ * Returns read data */
+static char *
+read_proc_stream(pid_t pid, FILE *fp, const cancellation_t *cancellation)
+{
+	char *buf = NULL;
+	size_t buf_len = 0;
+
+	char *line = NULL;
+	wait_for_data_from(pid, fp, /*fd=*/-1, cancellation);
+	while((line = read_line(fp, line)) != NULL)
+	{
+		(void)strappend(&buf, &buf_len, line);
+		wait_for_data_from(pid, fp, /*fd=*/-1, cancellation);
+	}
+	free(line);
+
+	return buf;
 }
 
 /* Deletes mount point by its path. */
diff --git a/src/int/vim.c b/src/int/vim.c
index ab36439..2034fc6 100644
--- a/src/int/vim.c
+++ b/src/int/vim.c
@@ -18,8 +18,6 @@
 
 #include "vim.h"
 
-#include <curses.h> /* FALSE curs_set() */
-
 #include <ctype.h> /* isspace() */
 #include <errno.h> /* errno */
 #include <stdio.h> /* FILE fclose() fprintf() fputs() snprintf() */
@@ -38,6 +36,7 @@
 #include "../utils/str.h"
 #include "../utils/string_array.h"
 #include "../utils/test_helpers.h"
+#include "../utils/utils.h"
 #include "../background.h"
 #include "../filelist.h"
 #include "../flist_sel.h"
@@ -55,32 +54,23 @@ static void dump_filenames(view_t *view, FILE *fp, int nfiles, char *files[]);
 int
 vim_format_help_cmd(const char topic[], char cmd[], size_t cmd_size)
 {
-	int bg;
-
-#ifndef _WIN32
-	char *const escaped_rtp = shell_like_escape(PACKAGE_DATA_DIR, 0);
-	char *const escaped_args = shell_like_escape(topic, 0);
-
-	snprintf(cmd, cmd_size,
-			"%s -c 'set runtimepath+=%s/vim-doc' -c help\\ %s -c only",
-			cfg_get_vicmd(&bg), escaped_rtp, escaped_args);
+	char *escaped_data = posix_like_escape(get_installed_data_dir(), /*type=*/0);
+	char *set_rtp = format_str("+set runtimepath+=%s/vim-doc", escaped_data);
+	free(escaped_data);
 
-	free(escaped_args);
-	free(escaped_rtp);
-#else
-	char exe_dir[PATH_MAX + 1];
-	char *escaped_rtp;
+	char *escaped_set_rtp = shell_arg_escape(set_rtp, curr_stats.shell_type);
+	free(set_rtp);
 
-	(void)get_exe_dir(exe_dir, sizeof(exe_dir));
-	escaped_rtp = shell_like_escape(exe_dir, 0);
+	char *help = format_str("+help %s", topic);
+	char *escaped_help = shell_arg_escape(help, curr_stats.shell_type);
+	free(help);
 
-	snprintf(cmd, cmd_size,
-			"%s -c \"set runtimepath+=%s/data/vim-doc\" -c \"help %s\" -c only",
-			cfg_get_vicmd(&bg), escaped_rtp, topic);
-
-	free(escaped_rtp);
-#endif
+	int bg;
+	snprintf(cmd, cmd_size, "%s %s %s -c only", cfg_get_vicmd(&bg),
+			escaped_set_rtp, escaped_help);
 
+	free(escaped_set_rtp);
+	free(escaped_help);
 	return bg;
 }
 
@@ -110,7 +100,8 @@ vim_edit_files(int nfiles, char *files[])
 	for(i = 0; i < nfiles; ++i)
 	{
 		char *const expanded_path = expand_tilde(files[i]);
-		char *const escaped = shell_like_escape(expanded_path, 0);
+		char *const escaped =
+			shell_arg_escape(expanded_path, curr_stats.shell_type);
 		(void)strappendch(&cmd, &len, ' ');
 		(void)strappend(&cmd, &len, escaped);
 		free(escaped);
@@ -201,11 +192,7 @@ vim_view_file(const char filename[], int line, int column, int allow_forking)
 		}
 	}
 
-#ifndef _WIN32
-	escaped = shell_like_escape(filename, 0);
-#else
-	escaped = (char *)enclose_in_dquotes(filename, curr_stats.shell_type);
-#endif
+	escaped = shell_arg_escape(filename, curr_stats.shell_type);
 
 	if(line < 0 && column < 0)
 		snprintf(cmd, sizeof(cmd), "%s %s %s", vicmd, fork_str, escaped);
@@ -215,17 +202,11 @@ vim_view_file(const char filename[], int line, int column, int allow_forking)
 		snprintf(cmd, sizeof(cmd), "%s %s \"+call cursor(%d, %d)\" %s", vicmd,
 				fork_str, line, column, escaped);
 
-#ifndef _WIN32
 	free(escaped);
-#endif
 
 	result = run_vim(cmd, bg && allow_forking, allow_forking);
 
-	/* The check is for tests. */
-	if(curr_stats.load_stage > 0)
-	{
-		curs_set(0);
-	}
+	ui_set_cursor(/*visibility=*/0);
 
 	return result;
 }
diff --git a/src/io/ioc.h b/src/io/ioc.h
index 4adcc5a..c761114 100644
--- a/src/io/ioc.h
+++ b/src/io/ioc.h
@@ -50,6 +50,7 @@ typedef enum
 	IO_RES_SUCCEEDED, /* Operation has succeeded. */
 	IO_RES_SKIPPED,   /* Operation was rejected by the user. */
 	IO_RES_FAILED,    /* Operation has failed. */
+	IO_RES_ABORTED,   /* Failure should be propagated up. */
 }
 IoRes;
 
diff --git a/src/io/iop.c b/src/io/iop.c
index afcbcce..402f44f 100644
--- a/src/io/iop.c
+++ b/src/io/iop.c
@@ -771,8 +771,8 @@ iop_ln_internal(io_args_t *args)
 	}
 
 	/* We're using os_system() below, hence ST_CMD is hard-coded. */
-	escaped_path = strdup(enclose_in_dquotes(path, ST_CMD));
-	escaped_target = strdup(enclose_in_dquotes(target, ST_CMD));
+	escaped_path = shell_arg_escape(path, ST_CMD);
+	escaped_target = shell_arg_escape(target, ST_CMD);
 	if(escaped_path == NULL || escaped_target == NULL)
 	{
 		(void)ioe_errlst_append(&args->result.errors, target, IO_ERR_UNKNOWN,
@@ -864,6 +864,7 @@ retry_wrapper(iop_func func, io_args_t *args)
 				break;
 
 			case IO_ECR_BREAK:
+				result = IO_RES_ABORTED;
 				ioe_errlst_splice(&orig_errlist, &args->result.errors);
 				break;
 
diff --git a/src/io/ior.c b/src/io/ior.c
index 613fb6b..5c85aa9 100644
--- a/src/io/ior.c
+++ b/src/io/ior.c
@@ -498,6 +498,8 @@ vr_from_io_res(IoRes result)
 			return VR_OK;
 		case IO_RES_FAILED:
 			return VR_ERROR;
+		case IO_RES_ABORTED:
+			return VR_CANCELLED;
 	}
 
 	return VR_ERROR;
diff --git a/src/io/private/traverser.c b/src/io/private/traverser.c
index d06205a..f806b80 100644
--- a/src/io/private/traverser.c
+++ b/src/io/private/traverser.c
@@ -26,7 +26,7 @@
 #include "../../utils/path.h"
 #include "../../utils/str.h"
 
-static int traverse_subtree(const char path[], subtree_visitor visitor,
+static VisitResult traverse_subtree(const char path[], subtree_visitor visitor,
 		void *param);
 
 IoRes
@@ -35,27 +35,33 @@ traverse(const char path[], subtree_visitor visitor, void *param)
 	/* Duplication with traverse_subtree(), but this way traverse_subtree() can
 	 * use information from dirent structure to save some operations. */
 
+	VisitResult visit_result;
+
 	/* Treat symbolic links to directories as files as well. */
 	if(is_symlink(path) || !is_dir(path))
 	{
-		return (visitor(path, VA_FILE, param) == VR_OK) ? IO_RES_SUCCEEDED
-		                                                : IO_RES_FAILED;
+		visit_result = visitor(path, VA_FILE, param);
 	}
 	else
 	{
-		return (traverse_subtree(path, visitor, param) == 0) ? IO_RES_SUCCEEDED
-		                                                     : IO_RES_FAILED;
+		visit_result = traverse_subtree(path, visitor, param);
+	}
+
+	switch(visit_result)
+	{
+		case VR_OK:        return IO_RES_SUCCEEDED;
+		case VR_CANCELLED: return IO_RES_ABORTED;
+
+		default:           return IO_RES_FAILED;
 	}
 }
 
-/* A generic subtree traversing.  Returns zero on success, otherwise non-zero is
- * returned. */
-static int
+/* A generic subtree traversing.  Returns status of visitation. */
+static VisitResult
 traverse_subtree(const char path[], subtree_visitor visitor, void *param)
 {
 	DIR *dir;
 	struct dirent *d;
-	int result;
 	VisitResult enter_result;
 
 	dir = os_opendir(path);
@@ -65,13 +71,13 @@ traverse_subtree(const char path[], subtree_visitor visitor, void *param)
 	}
 
 	enter_result = visitor(path, VA_DIR_ENTER, param);
-	if(enter_result == VR_ERROR)
+	if(enter_result == VR_ERROR || enter_result == VR_CANCELLED)
 	{
 		(void)os_closedir(dir);
 		return 1;
 	}
 
-	result = 0;
+	VisitResult result = VR_OK;
 	while((d = os_readdir(dir)) != NULL)
 	{
 		char *full_path;
@@ -85,7 +91,7 @@ traverse_subtree(const char path[], subtree_visitor visitor, void *param)
 		if(entry_is_link(full_path, d))
 		{
 			/* Treat symbolic links to directories as files as well. */
-			result = (visitor(full_path, VA_FILE, param) != VR_OK);
+			result = visitor(full_path, VA_FILE, param);
 		}
 		else if(entry_is_dir(full_path, d))
 		{
@@ -93,21 +99,20 @@ traverse_subtree(const char path[], subtree_visitor visitor, void *param)
 		}
 		else
 		{
-			result = (visitor(full_path, VA_FILE, param) != VR_OK);
+			result = visitor(full_path, VA_FILE, param);
 		}
 		free(full_path);
 
-		if(result != 0)
+		if(result != VR_OK)
 		{
 			break;
 		}
 	}
 	(void)os_closedir(dir);
 
-	if(result == 0 && enter_result != VR_SKIP_DIR_LEAVE &&
-			enter_result != VR_CANCELLED)
+	if(result == VR_OK && enter_result != VR_SKIP_DIR_LEAVE)
 	{
-		result = (visitor(path, VA_DIR_LEAVE, param) != VR_OK);
+		result = visitor(path, VA_DIR_LEAVE, param);
 	}
 
 	return result;
diff --git a/src/ipc.c b/src/ipc.c
index 2c8ec83..a3f2029 100644
--- a/src/ipc.c
+++ b/src/ipc.c
@@ -146,7 +146,6 @@ static char * get_the_only_target(const ipc_t *ipc);
 static char ** list_servers(const ipc_t *ipc, int *len);
 static int add_to_list(const char name[], const void *data, void *param);
 static const char * get_ipc_dir(void);
-static int sorter(const void *first, const void *second);
 #ifndef WIN32_PIPE_READ
 static int pipe_is_in_use(const char path[]);
 #endif
@@ -811,7 +810,7 @@ list_servers(const ipc_t *ipc, int *len)
 	}
 #endif
 
-	safe_qsort(data.lst, data.len, sizeof(*data.lst), &sorter);
+	safe_qsort(data.lst, data.len, sizeof(*data.lst), &strsorter);
 
 	*len = data.len;
 	return data.lst;
@@ -867,15 +866,6 @@ get_ipc_dir(void)
 #endif
 }
 
-/* Wraps strcmp() for use with qsort(). */
-static int
-sorter(const void *first, const void *second)
-{
-	const char *const *const a = first;
-	const char *const *const b = second;
-	return strcmp(*a, *b);
-}
-
 #ifndef WIN32_PIPE_READ
 
 /* Tries to open a pipe to check whether it has any readers or it's
diff --git a/src/lua/common.c b/src/lua/common.c
index c6f7152..f153bb4 100644
--- a/src/lua/common.c
+++ b/src/lua/common.c
@@ -18,11 +18,17 @@
 
 #include "common.h"
 
+#include "../compat/reallocarray.h"
 #include "../engine/options.h"
 #include "../engine/text_buffer.h"
+#include "../ui/ui.h"
+#include "../utils/utils.h"
 #include "lua/lauxlib.h"
 #include "lua/lua.h"
 
+static int deduplicate_ints(int array[], int count);
+static int int_sorter(const void *first, const void *second);
+
 int
 check_opt_arg(lua_State *lua, int arg_idx, int expected_type)
 {
@@ -166,7 +172,7 @@ void
 push_str_array(lua_State *lua, char *array[], int len)
 {
 	int i;
-	lua_newtable(lua);
+	lua_createtable(lua, len, /*nrec=*/0);
 	for(i = 0; i < len; ++i)
 	{
 		lua_pushstring(lua, array[i]);
@@ -174,5 +180,107 @@ push_str_array(lua_State *lua, char *array[], int len)
 	}
 }
 
+void
+make_metatable(lua_State *lua, const char name[])
+{
+	if(name == NULL)
+	{
+		lua_createtable(lua, /*narr=*/0, /*nrec=*/2);
+	}
+	else
+	{
+		luaL_newmetatable(lua, name);
+	}
+
+	lua_pushvalue(lua, -1);
+	lua_setfield(lua, -2, "__index");
+	lua_pushboolean(lua, 0);
+	lua_setfield(lua, -2, "__metatable");
+}
+
+int
+extract_indexes(lua_State *lua, view_t *view, int *count, int *indexes[])
+{
+	if(!lua_istable(lua, -1))
+	{
+		return 1;
+	}
+
+	if(lua_getfield(lua, -1, "indexes") != LUA_TTABLE)
+	{
+		lua_pop(lua, 1);
+		return 1;
+	}
+
+	lua_len(lua, -1);
+	*count = lua_tointeger(lua, -1);
+
+	*indexes = reallocarray(NULL, *count, sizeof((*indexes)[0]));
+	if(*indexes == NULL)
+	{
+		*count = 0;
+		lua_pop(lua, 2);
+		return 1;
+	}
+
+	int i = 0;
+	lua_pushnil(lua);
+	while(lua_next(lua, -3) != 0)
+	{
+		int idx = lua_tointeger(lua, -1) - 1;
+		/* XXX: Non-convertable to integer indexes are converted to (0 - 1) and
+		 *      thrown away by the next line. */
+		if(idx >= 0 && idx < view->list_rows)
+		{
+			(*indexes)[i++] = idx;
+		}
+		lua_pop(lua, 1);
+	}
+	*count = i;
+
+	*count = deduplicate_ints(*indexes, *count);
+
+	lua_pop(lua, 2);
+	return 0;
+}
+
+/* Removes duplicates from array of ints while sorting it.  Returns new array
+ * size. */
+static int
+deduplicate_ints(int array[], int count)
+{
+	if(count == 0)
+	{
+		return 0;
+	}
+
+	/* Sort list of indexes to simplify finding duplicates. */
+	safe_qsort(array, count, sizeof(array[0]), &int_sorter);
+
+	/* Drop duplicates from the list of indexes. */
+	int i;
+	int j = 1;
+	for(i = 1; i < count; ++i)
+	{
+		if(array[i] != array[j - 1])
+		{
+			array[j++] = array[i];
+		}
+	}
+
+	return j;
+}
+
+/* qsort() comparer that sorts ints.  Returns standard -1, 0, 1 for
+ * comparisons. */
+static int
+int_sorter(const void *first, const void *second)
+{
+	const int *a = first;
+	const int *b = second;
+
+	return (*a - *b);
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 : */
diff --git a/src/lua/common.h b/src/lua/common.h
index 1b9391a..e3e4b3d 100644
--- a/src/lua/common.h
+++ b/src/lua/common.h
@@ -20,6 +20,7 @@
 #define VIFM__LUA__COMMON_H__
 
 struct lua_State;
+struct view_t;
 
 /* Retrieves optional argument while checking its type and aborting (Lua does
  * longjmp()) if it doesn't match.  Returns non-zero if the argument is present
@@ -59,6 +60,18 @@ int set_opt(struct lua_State *lua, struct opt_t *opt);
 /* Creates an array of strings and leaves it on the top of the stack. */
 void push_str_array(struct lua_State *lua, char *array[], int len);
 
+/* Creates a metatable whose __index points to itself and which is opaque for
+ * Lua code (can't be read or set).  If name is NULL, the metatable isn't stored
+ * in the registry.  The metatable is left on the top of the stack. */
+void make_metatable(struct lua_State *lua, const char name[]);
+
+/* Extracts selected indexes from "indexes" field of the table at the top of Lua
+ * stack.  For valid "indexes" field, allocates an array, which should be freed
+ * by the caller.  Indexes are sorted and deduplicated.  Returns zero on success
+ * (valid "indexes" field) and non-zero on error. */
+int extract_indexes(struct lua_State *lua, struct view_t *view, int *count,
+		int *indexes[]);
+
 #endif /* VIFM__LUA__COMMON_H__ */
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/src/lua/vifm.c b/src/lua/vifm.c
new file mode 100644
index 0000000..8f42b75
--- /dev/null
+++ b/src/lua/vifm.c
@@ -0,0 +1,533 @@
+/* vifm
+ * Copyright (C) 2023 xaizek.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+#include "vifm.h"
+
+#include "../cfg/info.h"
+#include "../engine/options.h"
+#include "../modes/dialogs/msg_dialog.h"
+#include "../modes/cmdline.h"
+#include "../ui/statusbar.h"
+#include "../utils/fs.h"
+#include "../utils/path.h"
+#include "../utils/str.h"
+#include "../utils/utils.h"
+#include "../event_loop.h"
+#include "../filelist.h"
+#include "../filename_modifiers.h"
+#include "../running.h"
+#include "lua/lua.h"
+#include "lua/lauxlib.h"
+#include "api.h"
+#include "common.h"
+#include "vifm_cmds.h"
+#include "vifm_events.h"
+#include "vifm_handlers.h"
+#include "vifm_keys.h"
+#include "vifm_tabs.h"
+#include "vifm_viewcolumns.h"
+#include "vifmjob.h"
+#include "vifmview.h"
+#include "vlua_state.h"
+
+/* Lua API version. */
+#define API_VER_MAJOR 0
+#define API_VER_MINOR 1
+#define API_VER_PATCH 0
+
+/*
+ * This unit contains generic part of `vifm` global table.  Plugin-specific
+ * things are in vlua unit.
+ *
+ * Implementation for small things are here, others are in corresponding vifm_*
+ * or type-specific vifm* units and are only initialized here.
+ */
+
+/* Data passed to prompt callback in vifm.input(). */
+typedef struct
+{
+	int quit;       /* Exit flag for event loop. */
+	char *response; /* Result of the prompt. */
+}
+input_cb_data_t;
+
+static int VLUA_API(vifm_errordialog)(lua_State *lua);
+static int VLUA_API(vifm_escape)(lua_State *lua);
+static int VLUA_API(vifm_executable)(lua_State *lua);
+static int VLUA_API(vifm_exists)(lua_State *lua);
+static int VLUA_API(vifm_expand)(lua_State *lua);
+static int VLUA_API(vifm_fnamemodify)(lua_State *lua);
+static int VLUA_API(vifm_input)(lua_State *lua);
+static int VLUA_API(vifm_makepath)(lua_State *lua);
+static int VLUA_API(vifm_run)(lua_State *lua);
+static int VLUA_API(vifm_sessions_current)(lua_State *lua);
+
+static int VLUA_API(api_is_at_least)(lua_State *lua);
+static int VLUA_API(api_has)(lua_State *lua);
+
+static int VLUA_API(opts_global_index)(lua_State *lua);
+static int VLUA_API(opts_global_newindex)(lua_State *lua);
+
+static int VLUA_API(sb_info)(lua_State *lua);
+static int VLUA_API(sb_error)(lua_State *lua);
+static int VLUA_API(sb_quick)(lua_State *lua);
+
+VLUA_DECLARE_SAFE(vifm_errordialog);
+VLUA_DECLARE_SAFE(vifm_escape);
+VLUA_DECLARE_SAFE(vifm_executable);
+VLUA_DECLARE_SAFE(vifm_exists);
+VLUA_DECLARE_SAFE(vifm_expand);
+VLUA_DECLARE_SAFE(vifm_fnamemodify);
+VLUA_DECLARE_SAFE(vifm_input);
+VLUA_DECLARE_SAFE(vifm_makepath);
+VLUA_DECLARE_SAFE(vifm_run);
+VLUA_DECLARE_SAFE(vifm_sessions_current);
+
+VLUA_DECLARE_SAFE(api_is_at_least);
+VLUA_DECLARE_SAFE(api_has);
+
+VLUA_DECLARE_SAFE(opts_global_index);
+VLUA_DECLARE_UNSAFE(opts_global_newindex);
+
+VLUA_DECLARE_SAFE(sb_info);
+VLUA_DECLARE_SAFE(sb_error);
+VLUA_DECLARE_SAFE(sb_quick);
+
+/* These are defined in other units. */
+VLUA_DECLARE_UNSAFE(vifm_addcolumntype);
+VLUA_DECLARE_SAFE(vifm_addhandler);
+VLUA_DECLARE_SAFE(vifmview_currview);
+VLUA_DECLARE_SAFE(vifmview_otherview);
+VLUA_DECLARE_SAFE(vifmjob_new);
+
+static void input_builtin_cb(const char response[], void *arg);
+
+/* Functions of `vifm` global table. */
+static const struct luaL_Reg vifm_methods[] = {
+	{ "errordialog",   VLUA_REF(vifm_errordialog)   },
+	{ "escape",        VLUA_REF(vifm_escape)        },
+	{ "executable",    VLUA_REF(vifm_executable)    },
+	{ "exists",        VLUA_REF(vifm_exists)        },
+	{ "expand",        VLUA_REF(vifm_expand)        },
+	{ "fnamemodify",   VLUA_REF(vifm_fnamemodify)   },
+	{ "input",         VLUA_REF(vifm_input)         },
+	{ "makepath",      VLUA_REF(vifm_makepath)      },
+	{ "run",           VLUA_REF(vifm_run)           },
+
+	/* Defined in other units. */
+	{ "addcolumntype", VLUA_REF(vifm_addcolumntype) },
+	{ "addhandler",    VLUA_REF(vifm_addhandler)    },
+	{ "currview",      VLUA_REF(vifmview_currview)  },
+	{ "otherview",     VLUA_REF(vifmview_otherview) },
+	{ "startjob",      VLUA_REF(vifmjob_new)        },
+
+	{ NULL,            NULL                         }
+};
+
+/* Functions of `vifm.sb` table. */
+static const struct luaL_Reg sb_methods[] = {
+	{ "info",   VLUA_REF(sb_info)  },
+	{ "error",  VLUA_REF(sb_error) },
+	{ "quick",  VLUA_REF(sb_quick) },
+	{ NULL,     NULL               }
+};
+
+void
+vifm_init(lua_State *lua)
+{
+	vifmjob_init(lua);
+	vifmview_init(lua);
+
+	luaL_newlib(lua, vifm_methods);
+
+	/* Setup vifm.cmds. */
+	vifm_cmds_init(lua);
+	lua_setfield(lua, -2, "cmds");
+
+	/* Setup vifm.events. */
+	vifm_events_init(lua);
+	lua_setfield(lua, -2, "events");
+
+	/* Setup vifm.keys. */
+	vifm_keys_init(lua);
+	lua_setfield(lua, -2, "keys");
+
+	/* Setup vifm.tabs. */
+	vifm_tabs_init(lua);
+	lua_setfield(lua, -2, "tabs");
+
+	/* Setup vifm.opts. */
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/1); /* vifm.opts */
+	lua_newtable(lua);                  /* vifm.opts.global */
+	make_metatable(lua, /*name=*/NULL); /* metatable of vifm.opts.global */
+	lua_pushcfunction(lua, VLUA_REF(opts_global_index));
+	lua_setfield(lua, -2, "__index");
+	lua_pushcfunction(lua, VLUA_REF(opts_global_newindex));
+	lua_setfield(lua, -2, "__newindex");
+	lua_setmetatable(lua, -2);       /* vifm.opts.global */
+	lua_setfield(lua, -2, "global"); /* vifm.opts.global */
+	lua_setfield(lua, -2, "opts");   /* vifm.opts */
+
+	/* Setup vifm.plugins. */
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/1); /* vifm.plugins */
+	lua_newtable(lua);                            /* vifm.plugins.all */
+	lua_setfield(lua, -2, "all");
+	lua_setfield(lua, -2, "plugins");
+
+	/* Setup vifm.version. */
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/2); /* vifm.version */
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/1); /* vifm.app */
+	lua_pushstring(lua, VERSION);
+	lua_setfield(lua, -2, "str");                 /* vifm.app.str */
+	lua_setfield(lua, -2, "app");                 /* vifm.app */
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/5); /* vifm.api */
+	lua_pushinteger(lua, API_VER_MAJOR);
+	lua_setfield(lua, -2, "major");               /* vifm.api.major */
+	lua_pushinteger(lua, API_VER_MINOR);
+	lua_setfield(lua, -2, "minor");               /* vifm.api.minor */
+	lua_pushinteger(lua, API_VER_PATCH);
+	lua_setfield(lua, -2, "patch");               /* vifm.api.patch */
+	lua_pushcfunction(lua, VLUA_REF(api_has));
+	lua_setfield(lua, -2, "has");                 /* vifm.api.has */
+	lua_pushcfunction(lua, VLUA_REF(api_is_at_least));
+	lua_setfield(lua, -2, "atleast");             /* vifm.api.atleast */
+	lua_setfield(lua, -2, "api");                 /* vifm.api */
+	lua_setfield(lua, -2, "version");             /* vifm.version */
+
+	/* Setup vifm.sb. */
+	luaL_newlib(lua, sb_methods);
+	lua_setfield(lua, -2, "sb");
+
+	/* Setup vifm.sessions. */
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/1); /* vifm.sessions */
+	lua_pushcfunction(lua, VLUA_REF(vifm_sessions_current));
+	lua_setfield(lua, -2, "current");             /* vifm.sessions.current */
+	lua_setfield(lua, -2, "sessions");            /* vifm.sessions */
+}
+
+/* Member of `vifm` that displays an error dialog.  Doesn't return anything. */
+static int
+VLUA_API(vifm_errordialog)(lua_State *lua)
+{
+	const char *title = luaL_checkstring(lua, 1);
+	const char *msg = luaL_checkstring(lua, 2);
+	show_error_msg(title, msg);
+	return 0;
+}
+
+/* Member of `vifm` that escapes its input string.  Returns escaped string. */
+static int
+VLUA_API(vifm_escape)(lua_State *lua)
+{
+	const char *what = luaL_checkstring(lua, 1);
+	char *escaped = shell_arg_escape(what, curr_stats.shell_type);
+	lua_pushstring(lua, escaped);
+	free(escaped);
+	return 1;
+}
+
+/* Member of `vifm` that checks whether executable exists at absolute path or
+ * in directories listed in $PATH when path isn't absolute.  Checks for various
+ * executable extensions on Windows.  Returns a boolean. */
+static int
+VLUA_API(vifm_executable)(lua_State *lua)
+{
+	const char *path = luaL_checkstring(lua, 1);
+
+	int executable;
+	if(contains_slash(path))
+	{
+		executable = executable_exists(path);
+	}
+	else
+	{
+		executable = (find_cmd_in_path(path, 0UL, NULL) == 0);
+	}
+
+	lua_pushboolean(lua, executable);
+	return 1;
+}
+
+/* Member of `vifm` that checks whether specified path exists without resolving
+ * symbolic links.  Returns a boolean, which is true when path does exist. */
+static int
+VLUA_API(vifm_exists)(lua_State *lua)
+{
+	const char *path = luaL_checkstring(lua, 1);
+	lua_pushboolean(lua, path_exists(path, NODEREF));
+	return 1;
+}
+
+/* Member of `vifm` that expands macros and environment variables.  Returns the
+ * expanded string. */
+static int
+VLUA_API(vifm_expand)(lua_State *lua)
+{
+	const char *str = luaL_checkstring(lua, 1);
+
+	char *env_expanded = expand_envvars(str,
+			EEF_KEEP_ESCAPES | EEF_DOUBLE_PERCENTS);
+	char *full_expanded = ma_expand(env_expanded, NULL, NULL, MER_DISPLAY);
+	lua_pushstring(lua, full_expanded);
+	free(env_expanded);
+	free(full_expanded);
+
+	return 1;
+}
+
+/* Member of `vifm` that modifies path according to specifiers.  Returns
+ * modified path. */
+static int
+VLUA_API(vifm_fnamemodify)(lua_State *lua)
+{
+	const char *path = luaL_checkstring(lua, 1);
+	const char *modifiers = luaL_checkstring(lua, 2);
+	const char *base = luaL_optstring(lua, 3, flist_get_dir(curr_view));
+	lua_pushstring(lua, mods_apply(path, base, modifiers, 0));
+	return 1;
+}
+
+/* Member of `vifm` that asks user for input via a prompt.  Returns a string on
+ * success and nil on failure. */
+static int
+VLUA_API(vifm_input)(lua_State *lua)
+{
+	luaL_checktype(lua, 1, LUA_TTABLE);
+
+	check_field(lua, 1, "prompt", LUA_TSTRING);
+	const char *prompt = lua_tostring(lua, -1);
+
+	const char *initial = "";
+	if(check_opt_field(lua, 1, "initial", LUA_TSTRING))
+	{
+		initial = lua_tostring(lua, -1);
+	}
+
+	complete_cmd_func complete = NULL;
+	if(check_opt_field(lua, 1, "complete", LUA_TSTRING))
+	{
+		const char *value = lua_tostring(lua, -1);
+		if(strcmp(value, "dir") == 0)
+		{
+			complete = &modcline_complete_dirs;
+		}
+		else if(strcmp(value, "file") == 0)
+		{
+			complete = &modcline_complete_files;
+		}
+		else if(strcmp(value, "") != 0)
+		{
+			return luaL_error(lua, "Unrecognized value for `complete`: %s", value);
+		}
+	}
+
+	input_cb_data_t cb_data = { .quit = 0, .response = NULL };
+	modcline_prompt(prompt, initial, &input_builtin_cb, &cb_data, complete,
+			/*allow_ee=*/1);
+	event_loop(&cb_data.quit, /*manage_marking=*/0);
+
+	if(cb_data.response == NULL)
+	{
+		lua_pushnil(lua);
+	}
+	else
+	{
+		lua_pushstring(lua, cb_data.response);
+		free(cb_data.response);
+	}
+	return 1;
+}
+
+/* Callback invoked after prompt has finished. */
+static void
+input_builtin_cb(const char response[], void *arg)
+{
+	input_cb_data_t *data = arg;
+
+	update_string(&data->response, response);
+	data->quit = 1;
+}
+
+/* Member of `vifm` that creates a directory and all of its missing parent
+ * directories.  Returns a boolean, which is true on success. */
+static int
+VLUA_API(vifm_makepath)(lua_State *lua)
+{
+	const char *path = luaL_checkstring(lua, 1);
+	lua_pushboolean(lua, make_path(path, 0755) == 0);
+	return 1;
+}
+
+/* Runs an external command similar to :!. */
+static int
+VLUA_API(vifm_run)(lua_State *lua)
+{
+	luaL_checktype(lua, 1, LUA_TTABLE);
+
+	check_field(lua, 1, "cmd", LUA_TSTRING);
+	const char *cmd = lua_tostring(lua, -1);
+
+	int use_term_mux = 1;
+	if(check_opt_field(lua, 1, "usetermmux", LUA_TBOOLEAN))
+	{
+		use_term_mux = lua_toboolean(lua, -1);
+	}
+
+	ShellPause pause = PAUSE_ON_ERROR;
+	if(check_opt_field(lua, 1, "pause", LUA_TSTRING))
+	{
+		const char *value = lua_tostring(lua, -1);
+		if(strcmp(value, "never") == 0)
+		{
+			pause = PAUSE_NEVER;
+		}
+		else if(strcmp(value, "onerror") == 0)
+		{
+			pause = PAUSE_ON_ERROR;
+		}
+		else if(strcmp(value, "always") == 0)
+		{
+			pause = PAUSE_ALWAYS;
+		}
+		else
+		{
+			return luaL_error(lua, "Unrecognized value for `pause`: %s", value);
+		}
+	}
+
+	lua_pushinteger(lua, rn_shell(cmd, pause, use_term_mux, SHELL_BY_APP));
+	return 1;
+}
+
+/* Member of `vifm.sessions` that retrieves name of the current session.
+ * Returns string or nil. */
+static int
+VLUA_API(vifm_sessions_current)(lua_State *lua)
+{
+	if(sessions_active())
+	{
+		lua_pushstring(lua, sessions_current());
+	}
+	else
+	{
+		lua_pushnil(lua);
+	}
+	return 1;
+}
+
+/* Checks version of the API.  Returns a boolean. */
+static int
+VLUA_API(api_is_at_least)(lua_State *lua)
+{
+	const int major = luaL_checkinteger(lua, 1);
+	const int minor = luaL_optinteger(lua, 2, 0);
+	const int patch = luaL_optinteger(lua, 3, 0);
+
+	int result = 0;
+	if(major != API_VER_MAJOR)
+	{
+		result = (API_VER_MAJOR > major);
+	}
+	else if(minor != API_VER_MINOR)
+	{
+		result = (API_VER_MINOR > minor);
+	}
+	else
+	{
+		result = (API_VER_PATCH >= patch);
+	}
+
+	lua_pushboolean(lua, result);
+	return 1;
+}
+
+/* Performs tests for API features.  Returns a boolean. */
+static int
+VLUA_API(api_has)(lua_State *lua)
+{
+	(void)luaL_checkstring(lua, 1);
+	lua_pushboolean(lua, 0);
+	return 1;
+}
+
+/* Provides read access to global options by their name as
+ * `vifm.opts.global[name]`. */
+static int
+VLUA_API(opts_global_index)(lua_State *lua)
+{
+	const char *opt_name = luaL_checkstring(lua, 2);
+
+	opt_t *opt = vle_opts_find(opt_name, OPT_ANY);
+	if(opt == NULL || opt->scope == OPT_LOCAL)
+	{
+		return 0;
+	}
+
+	return get_opt(lua, opt);
+}
+
+/* Provides write access to global options by their name as
+ * `vifm.opts.global[name] = value`. */
+static int
+VLUA_API(opts_global_newindex)(lua_State *lua)
+{
+	const char *opt_name = luaL_checkstring(lua, 2);
+
+	opt_t *opt = vle_opts_find(opt_name, OPT_ANY);
+	if(opt == NULL || opt->scope == OPT_LOCAL)
+	{
+		return 0;
+	}
+
+	return set_opt(lua, opt);
+}
+
+/* Member of `vifm.sb` that prints a normal message on the status bar.  Doesn't
+ * return anything. */
+static int
+VLUA_API(sb_info)(lua_State *lua)
+{
+	const char *msg = luaL_checkstring(lua, 1);
+	ui_sb_msg(msg);
+	curr_stats.save_msg = 1;
+	return 0;
+}
+
+/* Member of `vifm.sb` that prints an error message on the status bar.  Doesn't
+ * return anything. */
+static int
+VLUA_API(sb_error)(lua_State *lua)
+{
+	const char *msg = luaL_checkstring(lua, 1);
+	ui_sb_err(msg);
+	curr_stats.save_msg = 1;
+	return 0;
+}
+
+/* Member of `vifm.sb` that prints status bar message that's not stored in
+ * history.  Doesn't return anything. */
+static int
+VLUA_API(sb_quick)(lua_State *lua)
+{
+	const char *msg = luaL_checkstring(lua, 1);
+	ui_sb_quick_msgf("%s", msg);
+	return 0;
+}
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 filetype=c : */
diff --git a/src/lua/vifm.h b/src/lua/vifm.h
new file mode 100644
index 0000000..6fcdd31
--- /dev/null
+++ b/src/lua/vifm.h
@@ -0,0 +1,30 @@
+/* vifm
+ * Copyright (C) 2023 xaizek.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+#ifndef VIFM__LUA__VIFM_H__
+#define VIFM__LUA__VIFM_H__
+
+struct lua_State;
+
+/* Produces `vifm` table.  Puts the table on the top of the stack. */
+void vifm_init(struct lua_State *lua);
+
+#endif /* VIFM__LUA__VIFM_H__ */
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 filetype=c : */
diff --git a/src/lua/vifm_cmds.c b/src/lua/vifm_cmds.c
index 2fedf4b..e1725fd 100644
--- a/src/lua/vifm_cmds.c
+++ b/src/lua/vifm_cmds.c
@@ -71,7 +71,7 @@ VLUA_API(cmds_add)(lua_State *lua)
 
 	int id = -1;
 
-	lua_newtable(lua);
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/1);
 	check_field(lua, 1, "handler", LUA_TFUNCTION);
 	lua_setfield(lua, -2, "handler");
 	if(check_opt_field(lua, 1, "complete", LUA_TFUNCTION))
@@ -190,7 +190,7 @@ lua_cmd_handler(const cmd_info_t *cmd_info)
 	from_pointer(lua, p->ptr);
 	lua_getfield(lua, -1, "handler");
 
-	lua_newtable(lua);
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/2);
 	lua_pushstring(lua, cmd_info->args);
 	lua_setfield(lua, -2, "args");
 	push_str_array(lua, cmd_info->argv, cmd_info->argc);
@@ -221,7 +221,7 @@ vifm_cmds_complete(lua_State *lua, const cmd_info_t *cmd_info, int arg_pos)
 		return 0;
 	}
 
-	lua_newtable(lua);
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/3);
 	lua_pushstring(lua, cmd_info->args);
 	lua_setfield(lua, -2, "args");
 	push_str_array(lua, cmd_info->argv, cmd_info->argc);
diff --git a/src/lua/vifm_events.c b/src/lua/vifm_events.c
new file mode 100644
index 0000000..77ea640
--- /dev/null
+++ b/src/lua/vifm_events.c
@@ -0,0 +1,204 @@
+/* vifm
+ * Copyright (C) 2022 xaizek.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+#include "vifm_events.h"
+
+#include <string.h> /* strcmp() */
+
+#include "../utils/macros.h"
+#include "../trash.h"
+#include "lua/lauxlib.h"
+#include "lua/lua.h"
+#include "api.h"
+#include "common.h"
+#include "vlua_cbacks.h"
+#include "vlua_state.h"
+
+static int VLUA_API(events_listen)(lua_State *lua);
+
+static void vifm_events_add(struct vlua_t *vlua, const char name[]);
+
+VLUA_DECLARE_SAFE(events_listen);
+
+/* Functions of `vifm.events` table. */
+static const luaL_Reg vifm_events_methods[] = {
+	{ "listen", VLUA_REF(events_listen) },
+	{ NULL,     NULL                    }
+};
+
+/* Mapping of operations onto their names in the API.  NULL means that the
+ * event is not (yet) reported. */
+static const char *const fsop_names[] = {
+	[OP_NONE]     = NULL,
+	[OP_USR]      = NULL,
+	[OP_REMOVE]   = "remove",
+	[OP_REMOVESL] = "remove",
+	[OP_COPY]     = "copy",
+	[OP_COPYF]    = "copy",
+	[OP_COPYA]    = "copy",
+	[OP_MOVE]     = "move",
+	[OP_MOVEF]    = "move",
+	[OP_MOVEA]    = "move",
+	[OP_MOVETMP1] = "move",
+	[OP_MOVETMP2] = "move",
+	[OP_MOVETMP3] = "move",
+	[OP_MOVETMP4] = "move",
+	[OP_CHOWN]    = NULL,
+	[OP_CHGRP]    = NULL,
+#ifndef _WIN32
+	[OP_CHMOD]    = NULL,
+	[OP_CHMODR]   = NULL,
+#else
+	[OP_ADDATTR]  = NULL,
+	[OP_SUBATTR]  = NULL,
+#endif
+	[OP_SYMLINK]  = "symlink",
+	[OP_SYMLINK2] = "symlink",
+	[OP_MKDIR]    = "create",
+	[OP_RMDIR]    = "remove",
+	[OP_MKFILE]   = "create",
+};
+ARRAY_GUARD(fsop_names, OP_COUNT);
+
+/* Address of this variable serves as a key in Lua table.  The table maps event
+ * name to set of its handlers (stored in keys, values are dummies). */
+static char events_key;
+
+void
+vifm_events_init(lua_State *lua)
+{
+	luaL_newlib(lua, vifm_events_methods);
+
+	vlua_t *vlua = get_state(lua);
+	vlua_state_make_table(vlua, &events_key);
+	vifm_events_add(vlua, "app.exit");
+	vifm_events_add(vlua, "app.fsop");
+}
+
+/* Registers a new event. */
+static void
+vifm_events_add(vlua_t *vlua, const char name[])
+{
+	vlua_state_get_table(vlua, &events_key);
+
+	if(lua_getfield(vlua->lua, -1, name) != LUA_TNIL)
+	{
+		assert(0 && "Event with the specified name already exists!");
+	}
+	lua_pop(vlua->lua, 1); /* nil */
+
+	lua_newtable(vlua->lua); /* event table */
+	lua_setfield(vlua->lua, -2, name); /* "create" event */
+	lua_pop(vlua->lua, 1); /* events table */
+}
+
+/* Member of `vifm.events` that adds subscription to an event. */
+static int
+VLUA_API(events_listen)(lua_State *lua)
+{
+	luaL_checktype(lua, 1, LUA_TTABLE);
+
+	check_field(lua, 1, "event", LUA_TSTRING);
+	const char *event = lua_tostring(lua, -1);
+
+	vlua_state_get_table(get_state(lua), &events_key);
+	if(lua_getfield(lua, -1, event) == LUA_TNIL)
+	{
+		return luaL_error(lua, "No such event: %s", event);
+	}
+
+	check_field(lua, 1, "handler", LUA_TFUNCTION);
+	lua_pushboolean(lua, 1);
+	lua_settable(lua, -3);
+	return 0;
+}
+
+void
+vifm_events_app_exit(vlua_t *vlua)
+{
+	vlua_state_get_table(vlua, &events_key); /* events table */
+	lua_getfield(vlua->lua, -1, "app.exit"); /* event table */
+	lua_remove(vlua->lua, -2); /* events table */
+
+	lua_pushnil(vlua->lua); /* key placeholder */
+	while(lua_next(vlua->lua, -2) != 0)
+	{
+		lua_pop(vlua->lua, 1); /* values are dummies */
+		lua_pushvalue(vlua->lua, -1); /* key is a handler */
+		vlua_cbacks_schedule(vlua, /*argc=*/0);
+	}
+
+	lua_pop(vlua->lua, 1); /* event table */
+}
+
+void
+vifm_events_app_fsop(vlua_t *vlua, OPS op, const char path[],
+		const char target[], void *extra, int dir)
+{
+	const char *fsop_name = fsop_names[op];
+	if(fsop_name == NULL)
+	{
+		/* We're not reporting this event. */
+		return;
+	}
+
+	vlua_state_get_table(vlua, &events_key); /* events table */
+	lua_getfield(vlua->lua, -1, "app.fsop"); /* event table */
+	lua_remove(vlua->lua, -2); /* events table */
+
+	int with_trash_flags = (strcmp(fsop_name, "move") == 0);
+	int from_trash = 0, to_trash = 0;
+	if(with_trash_flags)
+	{
+		from_trash = trash_has_path(path);
+		to_trash = trash_has_path(target);
+	}
+
+	lua_pushnil(vlua->lua); /* key placeholder */
+	while(lua_next(vlua->lua, -2) != 0)
+	{
+		lua_pop(vlua->lua, 1); /* values are dummies */
+		lua_pushvalue(vlua->lua, -1); /* key is a handler */
+
+		/* Not reusing the same argument, to not let handlers affect each other.
+		 * Alternative is to pass read-only table. */
+		lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/(with_trash_flags ? 6 : 4));
+		lua_pushstring(vlua->lua, fsop_name);
+		lua_setfield(vlua->lua, -2, "op");
+		lua_pushstring(vlua->lua, path);
+		lua_setfield(vlua->lua, -2, "path");
+		lua_pushstring(vlua->lua, target);
+		lua_setfield(vlua->lua, -2, "target");
+		lua_pushboolean(vlua->lua, dir);
+		lua_setfield(vlua->lua, -2, "isdir");
+		if(with_trash_flags)
+		{
+			lua_pushboolean(vlua->lua, from_trash);
+			lua_setfield(vlua->lua, -2, "fromtrash");
+			lua_pushboolean(vlua->lua, to_trash);
+			lua_setfield(vlua->lua, -2, "totrash");
+		}
+
+		vlua_cbacks_schedule(vlua, /*argc=*/1);
+	}
+
+	lua_pop(vlua->lua, 1); /* event table */
+}
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 : */
diff --git a/src/lua/vifm_events.h b/src/lua/vifm_events.h
new file mode 100644
index 0000000..beecb4b
--- /dev/null
+++ b/src/lua/vifm_events.h
@@ -0,0 +1,41 @@
+/* vifm
+ * Copyright (C) 2022 xaizek.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+#ifndef VIFM__LUA__VIFM_EVENTS_H__
+#define VIFM__LUA__VIFM_EVENTS_H__
+
+#include "../ops.h"
+
+struct lua_State;
+struct vlua_t;
+
+/* Produces `vifm.events` table.  Puts the table on the top of the stack. */
+void vifm_events_init(struct lua_State *lua);
+
+/* Schedules processing of an event of app exit. */
+void vifm_events_app_exit(struct vlua_t *vlua);
+
+/* Schedules processing of an event of an FS operation.  Target and extra
+ * parameters can be NULL. */
+void vifm_events_app_fsop(struct vlua_t *vlua, OPS op, const char path[],
+		const char target[], void *extra, int dir);
+
+#endif /* VIFM__LUA__VIFM_EVENTS_H__ */
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 : */
diff --git a/src/lua/vifm_handlers.c b/src/lua/vifm_handlers.c
index a96e997..ff172e7 100644
--- a/src/lua/vifm_handlers.c
+++ b/src/lua/vifm_handlers.c
@@ -41,6 +41,7 @@
 #include "vlua_state.h"
 
 static char * extract_handler_name(const char viewer[]);
+static char * run_format_handler(vlua_t *vlua, const char handler[]);
 static int run_editor_handler(vlua_t *vlua, const char handler[]);
 static int check_handler_name(vlua_t *vlua, const char name[]);
 
@@ -102,7 +103,7 @@ vifm_handlers_view(vlua_t *vlua, const char viewer[], const char path[],
 	assert(lua_getfield(vlua->lua, -1, "handler") == LUA_TFUNCTION &&
 			"Handler must be a function here.");
 
-	lua_newtable(vlua->lua);
+	lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/6);
 	lua_pushstring(vlua->lua, viewer);
 	lua_setfield(vlua->lua, -2, "command");
 	lua_pushstring(vlua->lua, path);
@@ -178,7 +179,7 @@ vifm_handlers_open(vlua_t *vlua, const char prog[], const dir_entry_t *entry)
 	assert(lua_getfield(vlua->lua, -1, "handler") == LUA_TFUNCTION &&
 			"Handler must be a function here.");
 
-	lua_newtable(vlua->lua);
+	lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/2);
 	lua_pushstring(vlua->lua, prog);
 	lua_setfield(vlua->lua, -2, "command");
 	vifmentry_new(vlua->lua, entry);
@@ -203,7 +204,38 @@ char *
 vifm_handlers_make_status_line(vlua_t *vlua, const char format[], view_t *view,
 		int width)
 {
-	char *name = extract_handler_name(format);
+	lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/2);
+	vifmview_new(vlua->lua, view);
+	lua_setfield(vlua->lua, -2, "view");
+	lua_pushinteger(vlua->lua, width);
+	lua_setfield(vlua->lua, -2, "width");
+
+	char *result = run_format_handler(vlua, format);
+	lua_pop(vlua->lua, 1);
+	return result;
+}
+
+char *
+vifm_handlers_make_tab_line(vlua_t *vlua, const char format[], int other,
+		int width)
+{
+	lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/2);
+	lua_pushboolean(vlua->lua, other);
+	lua_setfield(vlua->lua, -2, "other");
+	lua_pushinteger(vlua->lua, width);
+	lua_setfield(vlua->lua, -2, "width");
+
+	char *result = run_format_handler(vlua, format);
+	lua_pop(vlua->lua, 1);
+	return result;
+}
+
+/* Invokes a format handler.  Expects a table argument to it at the top of Lua
+ * stack.  Returns format string on success, otherwise NULL is returned. */
+static char *
+run_format_handler(vlua_t *vlua, const char handler[])
+{
+	char *name = extract_handler_name(handler);
 
 	/* Don't need lua_pcall() to handle errors, because no one should be able to
 	 * mess with internal tables. */
@@ -220,11 +252,7 @@ vifm_handlers_make_status_line(vlua_t *vlua, const char format[], view_t *view,
 	assert(lua_getfield(vlua->lua, -1, "handler") == LUA_TFUNCTION &&
 			"Handler must be a function here.");
 
-	lua_newtable(vlua->lua);
-	vifmview_new(vlua->lua, view);
-	lua_setfield(vlua->lua, -2, "view");
-	lua_pushinteger(vlua->lua, width);
-	lua_setfield(vlua->lua, -2, "width");
+	lua_pushvalue(vlua->lua, -4);
 
 	const int sm_cookie = vlua_state_safe_mode_on(vlua->lua);
 	if(lua_pcall(vlua->lua, 1, 1, 0) != LUA_OK)
@@ -251,27 +279,22 @@ vifm_handlers_make_status_line(vlua_t *vlua, const char format[], view_t *view,
 	}
 
 	const char *result = lua_tostring(vlua->lua, -1);
-	char *status_line = strdup(result == NULL ? "" : result);
+	char *format = strdup(result == NULL ? "" : result);
 
 	lua_pop(vlua->lua, 4);
 
-	return status_line;
+	return format;
 }
 
 int
 vifm_handlers_open_help(vlua_t *vlua, const char handler[], const char topic[])
 {
-#ifndef _WIN32
-	char vimdoc_dir[PATH_MAX + 1] = PACKAGE_DATA_DIR "/vim-doc";
-#else
-	char exe_dir[PATH_MAX + 1];
-	(void)get_exe_dir(exe_dir, sizeof(exe_dir));
-
 	char vimdoc_dir[PATH_MAX + 1];
-	snprintf(vimdoc_dir, sizeof(vimdoc_dir), "%s/vim-doc", exe_dir);
-#endif
+	snprintf(vimdoc_dir, sizeof(vimdoc_dir), "%s/vim-doc",
+			get_installed_data_dir());
+
+	lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/3);
 
-	lua_newtable(vlua->lua);
 	lua_pushstring(vlua->lua, "open-help");
 	lua_setfield(vlua->lua, -2, "action");
 
@@ -293,7 +316,7 @@ int
 vifm_handlers_edit_one(vlua_t *vlua, const char handler[], const char path[],
 		int line, int column, int must_wait)
 {
-	lua_newtable(vlua->lua);
+	lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/5);
 	lua_pushstring(vlua->lua, "edit-one");
 	lua_setfield(vlua->lua, -2, "action");
 	lua_pushstring(vlua->lua, path);
@@ -320,7 +343,7 @@ int
 vifm_handlers_edit_many(struct vlua_t *vlua, const char handler[],
 		char *files[], int nfiles)
 {
-	lua_newtable(vlua->lua);
+	lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/2);
 	lua_pushstring(vlua->lua, "edit-many");
 	lua_setfield(vlua->lua, -2, "action");
 	push_str_array(vlua->lua, files, nfiles);
@@ -335,7 +358,7 @@ int
 vifm_handlers_edit_list(vlua_t *vlua, const char handler[], char *entries[],
 		int nentries, int current, int quickfix_format)
 {
-	lua_newtable(vlua->lua);
+	lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/4);
 	lua_pushstring(vlua->lua, "edit-list");
 	lua_setfield(vlua->lua, -2, "action");
 	push_str_array(vlua->lua, entries, nentries);
@@ -446,7 +469,7 @@ VLUA_API(vifm_addhandler)(lua_State *lua)
 
 	char *full_name = format_str("#%s#%s", namespace, name);
 
-	vlua_state_get_table(vlua, &handlers_key);
+	vlua_state_get_table(vlua, &handlers_key); /* handlers table */
 
 	/* Check if handler already exists. */
 	if(lua_getfield(vlua->lua, -1, full_name) != LUA_TNIL)
@@ -455,12 +478,12 @@ VLUA_API(vifm_addhandler)(lua_State *lua)
 		lua_pushboolean(lua, 0);
 		return 1;
 	}
-	lua_pop(vlua->lua, 1);
+	lua_pop(vlua->lua, 1); /* nil */
 
-	lua_newtable(lua);
-	lua_pushvalue(lua, -3);
-	lua_setfield(lua, -2, "handler");
-	lua_setfield(lua, -2, full_name);
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/1); /* handler table */
+	lua_pushvalue(lua, -3);                       /* info.handler function */
+	lua_setfield(lua, -2, "handler");             /* handler.handler */
+	lua_setfield(lua, -2, full_name);             /* add to handlers table */
 
 	free(full_name);
 
diff --git a/src/lua/vifm_handlers.h b/src/lua/vifm_handlers.h
index 84dc6e7..88ebba3 100644
--- a/src/lua/vifm_handlers.h
+++ b/src/lua/vifm_handlers.h
@@ -53,6 +53,11 @@ void vifm_handlers_open(struct vlua_t *vlua, const char prog[],
 char * vifm_handlers_make_status_line(struct vlua_t *vlua, const char format[],
 		struct view_t *view, int width);
 
+/* Invokes tab line formatting handler.  Returns newly allocated string with
+ * tab line format. */
+char * vifm_handlers_make_tab_line(struct vlua_t *vlua, const char format[],
+		int other, int width);
+
 /* Invokes an editor handler to view Vim-style documentation.  Returns zero on
  * success and non-zero otherwise. */
 int vifm_handlers_open_help(struct vlua_t *vlua, const char handler[],
diff --git a/src/lua/vifm_keys.c b/src/lua/vifm_keys.c
index c49ba3e..aab0f30 100644
--- a/src/lua/vifm_keys.c
+++ b/src/lua/vifm_keys.c
@@ -18,16 +18,15 @@
 
 #include "vifm_keys.h"
 
+#include <stdlib.h> /* free() */
 #include <wchar.h>
 
-#include "../compat/reallocarray.h"
 #include "../engine/keys.h"
 #include "../modes/modes.h"
 #include "../ui/statusbar.h"
 #include "../ui/ui.h"
 #include "../utils/macros.h"
 #include "../utils/str.h"
-#include "../utils/utils.h"
 #include "../bracket_notation.h"
 #include "../status.h"
 #include "lua/lauxlib.h"
@@ -44,9 +43,6 @@ static void parse_modes(vlua_t *vlua, char modes[MODES_COUNT]);
 static void lua_key_handler(key_info_t key_info, keys_info_t *keys_info);
 static void build_handler_args(lua_State *lua, key_info_t key_info,
 		const keys_info_t *keys_info);
-static int extract_indexes(lua_State *lua, keys_info_t *keys_info);
-static int deduplicate_ints(int array[], int count);
-static int int_sorter(const void *first, const void *second);
 
 /* Functions of `vifm.keys` table. */
 static const luaL_Reg vifm_keys_methods[] = {
@@ -121,7 +117,7 @@ VLUA_API(keys_add)(lua_State *lua)
 	check_field(lua, 1, "modes", LUA_TTABLE);
 	parse_modes(vlua, modes);
 
-	lua_newtable(lua);
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/2);
 	check_field(lua, 1, "handler", LUA_TFUNCTION);
 	lua_setfield(lua, -2, "handler");
 	lua_pushboolean(lua, is_selector);
@@ -169,6 +165,10 @@ parse_modes(vlua_t *vlua, char modes[MODES_COUNT])
 		{
 			modes[CMDLINE_MODE] = 1;
 		}
+		else if(strcmp(name, "nav") == 0)
+		{
+			modes[NAV_MODE] = 1;
+		}
 		else if(strcmp(name, "normal") == 0)
 		{
 			modes[NORMAL_MODE] = 1;
@@ -239,9 +239,17 @@ lua_key_handler(key_info_t key_info, keys_info_t *keys_info)
 		return;
 	}
 
-	if(is_selector && extract_indexes(lua, keys_info) == 0)
+	if(is_selector)
 	{
-		keys_info->count = deduplicate_ints(keys_info->indexes, keys_info->count);
+		if(extract_indexes(lua, curr_view, &keys_info->count,
+					&keys_info->indexes) == 0)
+		{
+			if(keys_info->count == 0)
+			{
+				free(keys_info->indexes);
+				keys_info->indexes = NULL;
+			}
+		}
 	}
 
 	lua_pop(lua, 3);
@@ -252,7 +260,7 @@ static void
 build_handler_args(lua_State *lua, key_info_t key_info,
 		const keys_info_t *keys_info)
 {
-	lua_newtable(lua);
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/3);
 
 	if(key_info.count == NO_COUNT_GIVEN)
 	{
@@ -278,7 +286,7 @@ build_handler_args(lua_State *lua, key_info_t key_info,
 	if(keys_info->selector)
 	{
 		int i;
-		lua_newtable(lua);
+		lua_createtable(lua, keys_info->count, /*nrec=*/0);
 		for(i = 0; i < keys_info->count; ++i)
 		{
 			lua_pushinteger(lua, keys_info->indexes[i] + 1);
@@ -294,94 +302,5 @@ build_handler_args(lua_State *lua, key_info_t key_info,
 	}
 }
 
-/* Extracts selected indexes from "indexes" field of the table at the top of
- * Lua stack.  Returns zero on success and non-zero on error. */
-static int
-extract_indexes(lua_State *lua, keys_info_t *keys_info)
-{
-	if(!lua_istable(lua, -1))
-	{
-		return 1;
-	}
-
-	if(lua_getfield(lua, -1, "indexes") != LUA_TTABLE)
-	{
-		lua_pop(lua, 1);
-		return 1;
-	}
-
-	lua_len(lua, -1);
-	keys_info->count = lua_tointeger(lua, -1);
-
-	keys_info->indexes = reallocarray(NULL, keys_info->count,
-			sizeof(keys_info->indexes[0]));
-	if(keys_info->indexes == NULL)
-	{
-		keys_info->count = 0;
-		lua_pop(lua, 2);
-		return 1;
-	}
-
-	int i = 0;
-	lua_pushnil(lua);
-	while(lua_next(lua, -3) != 0)
-	{
-		int idx = lua_tointeger(lua, -1) - 1;
-		if(idx >= 0 && idx < curr_view->list_rows)
-		{
-			keys_info->indexes[i++] = idx;
-		}
-		lua_pop(lua, 1);
-	}
-	keys_info->count = i;
-
-	if(keys_info->count == 0)
-	{
-		free(keys_info->indexes);
-		keys_info->indexes = NULL;
-	}
-
-	lua_pop(lua, 2);
-	return 0;
-}
-
-/* Removes duplicates from array of ints while sorting it.  Returns new array
- * size. */
-static int
-deduplicate_ints(int array[], int count)
-{
-	if(count == 0)
-	{
-		return 0;
-	}
-
-	/* Sort list of indexes to simplify finding duplicates. */
-	safe_qsort(array, count, sizeof(array[0]), &int_sorter);
-
-	/* Drop duplicates from the list of indexes. */
-	int i;
-	int j = 1;
-	for(i = 1; i < count; ++i)
-	{
-		if(array[i] != array[j - 1])
-		{
-			array[j++] = array[i];
-		}
-	}
-
-	return j;
-}
-
-/* qsort() comparer that sorts ints.  Returns standard -1, 0, 1 for
- * comparisons. */
-static int
-int_sorter(const void *first, const void *second)
-{
-	const int *a = first;
-	const int *b = second;
-
-	return (*a - *b);
-}
-
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 : */
diff --git a/src/lua/vifm_tabs.c b/src/lua/vifm_tabs.c
index fa8b6f2..9da8613 100644
--- a/src/lua/vifm_tabs.c
+++ b/src/lua/vifm_tabs.c
@@ -51,7 +51,7 @@ vifm_tabs_init(lua_State *lua)
 	vifmtab_init(lua);
 }
 
-/* Member of `vifm.tabs` that number of tabs.  Returns an integer. */
+/* Member of `vifm.tabs` that retrieves number of tabs.  Returns an integer. */
 static int
 VLUA_API(tabs_getcount)(lua_State *lua)
 {
diff --git a/src/lua/vifm_viewcolumns.c b/src/lua/vifm_viewcolumns.c
index da07e0b..f4bef87 100644
--- a/src/lua/vifm_viewcolumns.c
+++ b/src/lua/vifm_viewcolumns.c
@@ -125,15 +125,15 @@ VLUA_API(vifm_addcolumntype)(lua_State *lua)
 	}
 
 	int column_id = viewcolumn_next_id++;
-	vlua_state_get_table(vlua, &viewcolumns_key);
-	lua_newtable(lua);
+	vlua_state_get_table(vlua, &viewcolumns_key); /* viewcolumns table */
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/2); /* viewcolumn table */
 	lua_pushinteger(lua, column_id);
 	lua_setfield(lua, -2, "id");
 	lua_pushboolean(lua, is_primary);
 	lua_setfield(lua, -2, "isprimary");
-	lua_pushvalue(lua, -1);
-	lua_setfield(lua, -3, name);
-	lua_seti(lua, -2, column_id);
+	lua_pushvalue(lua, -1);                       /* viewcolumn table */
+	lua_setfield(lua, -3, name);                  /* viewcolumns[name] */
+	lua_seti(lua, -2, column_id);                 /* viewcolumns[id] */
 
 	int error = columns_add_column_desc(column_id, &lua_viewcolumn_handler, data);
 	if(error)
@@ -187,7 +187,7 @@ lua_viewcolumn_handler(void *data, size_t buf_len, char buf[],
 
 	from_pointer(lua, p->ptr);
 
-	lua_newtable(lua);
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/2);
 
 	lua_pushinteger(lua, info->width);
 	lua_setfield(lua, -2, "width");
diff --git a/src/lua/vifmentry.c b/src/lua/vifmentry.c
index 0e445bc..52bd6c9 100644
--- a/src/lua/vifmentry.c
+++ b/src/lua/vifmentry.c
@@ -29,6 +29,7 @@
 #include "lua/lauxlib.h"
 #include "lua/lua.h"
 #include "api.h"
+#include "common.h"
 
 /* User data of view entry object. */
 typedef struct
@@ -55,9 +56,7 @@ static const luaL_Reg vifmentry_methods[] = {
 void
 vifmentry_init(lua_State *lua)
 {
-	luaL_newmetatable(lua, "VifmEntry");
-	lua_pushvalue(lua, -1);
-	lua_setfield(lua, -2, "__index");
+	make_metatable(lua, "VifmEntry");
 	luaL_setfuncs(lua, vifmentry_methods, 0);
 	lua_pop(lua, 1);
 }
@@ -65,7 +64,7 @@ vifmentry_init(lua_State *lua)
 void
 vifmentry_new(lua_State *lua, const dir_entry_t *entry)
 {
-	lua_newtable(lua);
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/16); /* entry */
 
 	lua_pushstring(lua, entry->name);
 	lua_setfield(lua, -2, "name");
@@ -81,6 +80,8 @@ vifmentry_new(lua_State *lua, const dir_entry_t *entry)
 	lua_setfield(lua, -2, "ctime");
 	lua_pushstring(lua, get_type_str(entry->type));
 	lua_setfield(lua, -2, "type");
+	lua_pushboolean(lua, fentry_is_dir(entry));
+	lua_setfield(lua, -2, "isdir");
 
 	int match = (entry->search_match != 0);
 	lua_pushboolean(lua, match);
@@ -98,12 +99,12 @@ vifmentry_new(lua_State *lua, const dir_entry_t *entry)
 
 	const char *prefix, *suffix;
 	ui_get_decors(entry, &prefix, &suffix);
-	lua_newtable(lua);
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/2); /* entry.classify */
 	lua_pushstring(lua, prefix);
-	lua_setfield(lua, -2, "prefix");
+	lua_setfield(lua, -2, "prefix");   /* entry.classify.prefix */
 	lua_pushstring(lua, suffix);
-	lua_setfield(lua, -2, "suffix");
-	lua_setfield(lua, -2, "classify");
+	lua_setfield(lua, -2, "suffix");   /* entry.classify.suffix */
+	lua_setfield(lua, -2, "classify"); /* entry.classify */
 
 	vifm_entry_t *vifm_entry = lua_newuserdatauv(lua, sizeof(*vifm_entry), 0);
 	luaL_getmetatable(lua, "VifmEntry");
diff --git a/src/lua/vifmjob.c b/src/lua/vifmjob.c
index 60ee104..8578741 100644
--- a/src/lua/vifmjob.c
+++ b/src/lua/vifmjob.c
@@ -18,6 +18,7 @@
 
 #include "vifmjob.h"
 
+#include <assert.h> /* assert() */
 #include <stdio.h> /* fclose() */
 #include <stdlib.h> /* free() */
 #include <string.h> /* strcmp() */
@@ -29,6 +30,8 @@
 #include "lua/lua.h"
 #include "api.h"
 #include "common.h"
+#include "vlua_cbacks.h"
+#include "vlua_state.h"
 
 /* User data of file stream associated with job's output stream. */
 typedef struct
@@ -54,7 +57,7 @@ static int VLUA_API(vifmjob_exitcode)(lua_State *lua);
 static int VLUA_API(vifmjob_stdin)(lua_State *lua);
 static int VLUA_API(vifmjob_stdout)(lua_State *lua);
 static int VLUA_API(vifmjob_errors)(lua_State *lua);
-static void job_stream_gc(lua_State *lua, job_stream_t *js);
+static void job_exit_cb(struct bg_job_t *job, void *arg);
 static job_stream_t * job_stream_open(lua_State *lua, bg_job_t *job,
 		FILE *stream);
 static void job_stream_close(lua_State *lua, job_stream_t *js);
@@ -78,19 +81,40 @@ static const luaL_Reg vifmjob_methods[] = {
 	{ NULL,       NULL                       }
 };
 
+/* Address of this variable serves as a key in Lua table. */
+static char jobs_key;
+
 void
 vifmjob_init(lua_State *lua)
 {
-	luaL_newmetatable(lua, "VifmJob");
-	lua_pushvalue(lua, -1);
-	lua_setfield(lua, -2, "__index");
+	make_metatable(lua, "VifmJob");
 	luaL_setfuncs(lua, vifmjob_methods, 0);
 	lua_pop(lua, 1);
+
+	vlua_state_make_table(get_state(lua), &jobs_key);
+}
+
+void
+vifmjob_finish(lua_State *lua)
+{
+	vlua_state_get_table(get_state(lua), &jobs_key);
+	lua_pushnil(lua);
+	while(lua_next(lua, -2) != 0)
+	{
+		lua_pop(lua, 1);
+
+		bg_job_t *job = lua_touserdata(lua, -1);
+		bg_job_set_exit_cb(job, NULL, NULL);
+	}
+
+	lua_pop(lua, 1);
 }
 
 int
 VLUA_API(vifmjob_new)(lua_State *lua)
 {
+	vlua_t *vlua = get_state(lua);
+
 	luaL_checktype(lua, 1, LUA_TTABLE);
 
 	check_field(lua, 1, "cmd", LUA_TSTRING);
@@ -130,6 +154,8 @@ VLUA_API(vifmjob_new)(lua_State *lua)
 		descr = lua_tostring(lua, -1);
 	}
 
+	int with_on_exit = check_opt_field(lua, 1, "onexit", LUA_TFUNCTION);
+
 	bg_job_t *job = bg_run_external_job(cmd, flags);
 	if(job == NULL)
 	{
@@ -146,12 +172,80 @@ VLUA_API(vifmjob_new)(lua_State *lua)
 	luaL_getmetatable(lua, "VifmJob");
 	lua_setmetatable(lua, -2);
 
+	/* Map job onto a table describing it in Lua. */
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/(with_on_exit ? 2 : 1));
+	lua_pushvalue(lua, -2);
+	lua_setfield(lua, -2, "obj");
+	if(with_on_exit)
+	{
+		lua_pushvalue(lua, -3);
+		lua_setfield(lua, -2, "onexit");
+	}
+	vlua_state_get_table(vlua, &jobs_key);
+	lua_pushlightuserdata(lua, job);
+	lua_pushvalue(lua, -3);
+	lua_settable(lua, -3);
+	lua_pop(lua, 2);
+
+	bg_job_set_exit_cb(job, &job_exit_cb, vlua);
+
 	data->job = job;
 	data->input = NULL;
 	data->output = NULL;
 	return 1;
 }
 
+/* Handles job's exit by closing its streams and doing some cleanup. */
+static void
+job_exit_cb(struct bg_job_t *job, void *arg)
+{
+	vlua_t *vlua = arg;
+
+	/* Find vifm_job_t that corresponds to the job. */
+	vlua_state_get_table(vlua, &jobs_key);
+	lua_pushlightuserdata(vlua->lua, job);
+	if(lua_gettable(vlua->lua, -2) != LUA_TTABLE)
+	{
+		assert(0 && "Exited job has no associated Lua job data!");
+		lua_pop(vlua->lua, 2);
+		return;
+	}
+
+	int with_on_exit = (lua_getfield(vlua->lua, -1, "onexit") == LUA_TFUNCTION);
+
+	lua_getfield(vlua->lua, -2, "obj");
+	vifm_job_t *vifm_job = lua_touserdata(vlua->lua, -1);
+
+	/* Remove the table entry we've just used. */
+	lua_pushlightuserdata(vlua->lua, job);
+	lua_pushnil(vlua->lua);
+	lua_settable(vlua->lua, -6);
+
+	/* Close input and output streams to make them error on use. */
+
+	if(vifm_job->input != NULL)
+	{
+		job_stream_close(vlua->lua, vifm_job->input);
+		vifm_job->input = NULL;
+	}
+
+	if(vifm_job->output != NULL)
+	{
+		job_stream_close(vlua->lua, vifm_job->output);
+		vifm_job->output = NULL;
+	}
+
+	if(with_on_exit)
+	{
+		vlua_cbacks_schedule(vlua, /*argc=*/1);
+		lua_pop(vlua->lua, 2);
+	}
+	else
+	{
+		lua_pop(vlua->lua, 4);
+	}
+}
+
 /* Method of of VifmJob that frees associated resources.  Doesn't return
  * anything. */
 static int
@@ -162,11 +256,11 @@ VLUA_API(vifmjob_gc)(lua_State *lua)
 
 	if(vifm_job->input != NULL)
 	{
-		job_stream_gc(lua, vifm_job->input);
+		drop_pointer(lua, vifm_job->input->obj);
 	}
 	if(vifm_job->output != NULL)
 	{
-		job_stream_gc(lua, vifm_job->output);
+		drop_pointer(lua, vifm_job->output->obj);
 	}
 
 	return 0;
@@ -179,18 +273,32 @@ VLUA_API(vifmjob_wait)(lua_State *lua)
 {
 	vifm_job_t *vifm_job = luaL_checkudata(lua, 1, "VifmJob");
 
-	/* Close Lua input stream to avoid situation when the job is blocked on
-	 * read. */
+	/* Close input stream to avoid situation when the job is blocked on read. */
 	if(vifm_job->input != NULL)
 	{
 		job_stream_close(lua, vifm_job->input);
+		vifm_job->input = NULL;
+
+		/* The stream might have been closed explicitly earlier. */
+		if(vifm_job->job->input != NULL)
+		{
+			fclose(vifm_job->job->input);
+			vifm_job->job->input = NULL;
+		}
 	}
 
-	/* Close Lua output stream to avoid situation when the job is blocked on
-	 * write. */
+	/* Close output stream to avoid situation when the job is blocked on write. */
 	if(vifm_job->output != NULL)
 	{
 		job_stream_close(lua, vifm_job->output);
+		vifm_job->output = NULL;
+
+		/* The stream might have been closed explicitly earlier. */
+		if(vifm_job->job->output != NULL)
+		{
+			fclose(vifm_job->job->output);
+			vifm_job->job->output = NULL;
+		}
 	}
 
 	if(bg_job_wait(vifm_job->job) != 0)
@@ -301,15 +409,6 @@ VLUA_API(vifmjob_errors)(lua_State *lua)
 	return 1;
 }
 
-/* Frees job stream when its parent is garbage collected. */
-static void
-job_stream_gc(lua_State *lua, job_stream_t *js)
-{
-	drop_pointer(lua, js->obj);
-	bg_job_decref(js->job);
-	js->job = NULL;
-}
-
 /* Creates a job stream.  Returns a pointer to new user data. */
 static job_stream_t *
 job_stream_open(lua_State *lua, bg_job_t *job, FILE *stream)
@@ -332,8 +431,13 @@ job_stream_open(lua_State *lua, bg_job_t *job, FILE *stream)
 static void
 job_stream_close(lua_State *lua, job_stream_t *js)
 {
-	js->lua_stream.closef = NULL;
-	bg_job_decref(js->job);
+	/* The stream might have already been closed from Lua. */
+	if(js->lua_stream.closef != NULL)
+	{
+		js->lua_stream.closef = NULL;
+		bg_job_decref(js->job);
+	}
+
 	drop_pointer(lua, js->obj);
 }
 
@@ -346,18 +450,17 @@ VLUA_IMPL(jobstream_closef)(lua_State *lua)
 
 	int stat = 1;
 
-	if(js->job != NULL)
+	if(js->lua_stream.f == js->job->input)
 	{
-		if(js->lua_stream.f == js->job->input)
-		{
-			stat = (fclose(js->job->input) == 0);
-			js->job->input = NULL;
-		}
-		else if(js->lua_stream.f == js->job->output)
-		{
-			stat = (fclose(js->job->output) == 0);
-			js->job->output = NULL;
-		}
+		stat = (fclose(js->job->input) == 0);
+		js->job->input = NULL;
+		bg_job_decref(js->job);
+	}
+	else if(js->lua_stream.f == js->job->output)
+	{
+		stat = (fclose(js->job->output) == 0);
+		js->job->output = NULL;
+		bg_job_decref(js->job);
 	}
 
 	return luaL_fileresult(lua, stat, NULL);
diff --git a/src/lua/vifmjob.h b/src/lua/vifmjob.h
index 64cf29e..f8f0355 100644
--- a/src/lua/vifmjob.h
+++ b/src/lua/vifmjob.h
@@ -26,6 +26,9 @@ struct lua_State;
 /* Initializes VifmJob type unit. */
 void vifmjob_init(struct lua_State *lua);
 
+/* Cleans up after this unit. */
+void vifmjob_finish(struct lua_State *lua);
+
 /* Starts an external application as detached from a terminal.  Returns an
  * object of VifmJob type or raises an error. */
 int VLUA_API(vifmjob_new)(struct lua_State *lua);
diff --git a/src/lua/vifmtab.c b/src/lua/vifmtab.c
index 3bdbcc7..cc6b7c1 100644
--- a/src/lua/vifmtab.c
+++ b/src/lua/vifmtab.c
@@ -27,13 +27,16 @@
 #include "common.h"
 #include "vifmview.h"
 
+/* Pointer to a function used to traverse tabs. */
+typedef int (*tab_search_f)(struct view_t *side, int idx, tab_info_t *tab_info);
+
 static int VLUA_API(vifmtab_getlayout)(lua_State *lua);
 static int VLUA_API(vifmtab_getname)(lua_State *lua);
 static int VLUA_API(vifmtab_getview)(lua_State *lua);
 
 static void find_tab(lua_State *lua, unsigned int id, tab_info_t *tab_info);
 static void find_side_tab(lua_State *lua, unsigned int id, tab_info_t *tab_info,
-		view_t *side);
+		view_t *side, tab_search_f tab_search);
 
 VLUA_DECLARE_SAFE(vifmtab_getlayout);
 VLUA_DECLARE_SAFE(vifmtab_getname);
@@ -50,9 +53,7 @@ static const luaL_Reg vifmtab_methods[] = {
 void
 vifmtab_init(lua_State *lua)
 {
-	luaL_newmetatable(lua, "VifmTab");
-	lua_pushvalue(lua, -1);
-	lua_setfield(lua, -2, "__index");
+	make_metatable(lua, "VifmTab");
 	luaL_setfuncs(lua, vifmtab_methods, 0);
 	lua_pop(lua, 1);
 }
@@ -108,7 +109,7 @@ VLUA_API(vifmtab_getlayout)(lua_State *lua)
 	tab_info_t tab_info;
 	find_tab(lua, *id, &tab_info);
 
-	lua_newtable(lua);
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/2);
 	lua_pushboolean(lua, tab_info.layout.only_mode);
 	lua_setfield(lua, -2, "only");
 	if(!tab_info.layout.only_mode)
@@ -139,7 +140,9 @@ VLUA_API(vifmtab_getview)(lua_State *lua)
 {
 	const unsigned int *id = luaL_checkudata(lua, 1, "VifmTab");
 
-	int other_side = 0;
+	/* Querying active pane of a global tab by default. */
+	view_t *side = curr_view;
+	tab_search_f tab_search = tabs_get;
 	if(check_opt_arg(lua, 2, LUA_TTABLE) &&
 			check_opt_field(lua, 2, "pane", LUA_TNUMBER))
 	{
@@ -148,7 +151,10 @@ VLUA_API(vifmtab_getview)(lua_State *lua)
 		{
 			return luaL_error(lua, "%s", "pane field is not in the range [1; 2]");
 		}
-		other_side = (pane == 2);
+
+		/* Querying specific side of a global tab. */
+		side = (pane == 1 ? &lwin : &rwin);
+		tab_search = tabs_enum;
 	}
 
 	tab_info_t tab_info;
@@ -158,7 +164,7 @@ VLUA_API(vifmtab_getview)(lua_State *lua)
 	}
 	else
 	{
-		find_side_tab(lua, *id, &tab_info, other_side ? other_view : curr_view);
+		find_side_tab(lua, *id, &tab_info, side, tab_search);
 	}
 
 	vifmview_new(lua, tab_info.view);
@@ -186,10 +192,10 @@ find_tab(lua_State *lua, unsigned int id, tab_info_t *tab_info)
  * pointer or aborts (Lua does longjmp()) if the tab doesn't exist anymore. */
 static void
 find_side_tab(lua_State *lua, unsigned int id, tab_info_t *tab_info,
-		view_t *side)
+		view_t *side, tab_search_f tab_search)
 {
 	int i;
-	for(i = 0; tabs_enum(side, i, tab_info); ++i)
+	for(i = 0; tab_search(side, i, tab_info); ++i)
 	{
 		if(tab_info->id == id)
 		{
diff --git a/src/lua/vifmview.c b/src/lua/vifmview.c
index bcd6f4f..bfe4894 100644
--- a/src/lua/vifmview.c
+++ b/src/lua/vifmview.c
@@ -18,12 +18,17 @@
 
 #include "vifmview.h"
 
+#include <stdlib.h> /* free() */
 #include <string.h> /* strcmp() */
 
+#include "../engine/mode.h"
 #include "../engine/options.h"
+#include "../modes/modes.h"
+#include "../modes/visual.h"
 #include "../ui/tabs.h"
 #include "../ui/ui.h"
 #include "../filelist.h"
+#include "../flist_sel.h"
 #include "../opt_handlers.h"
 #include "lua/lauxlib.h"
 #include "lua/lua.h"
@@ -44,6 +49,9 @@ static int VLUA_IMPL(get_opt_wrapper)(lua_State *lua);
 static int VLUA_IMPL(set_opt_wrapper)(lua_State *lua);
 static int VLUA_API(vifmview_cd)(lua_State *lua);
 static int VLUA_API(vifmview_entry)(lua_State *lua);
+static int VLUA_API(vifmview_select)(lua_State *lua);
+static int VLUA_API(vifmview_unselect)(lua_State *lua);
+static int select_unselect(lua_State *lua, int select);
 static view_t * check_view(lua_State *lua);
 static view_t * find_view(lua_State *lua, unsigned int id);
 
@@ -54,18 +62,22 @@ VLUA_DECLARE_SAFE(locopts_index);
 VLUA_DECLARE_UNSAFE(locopts_newindex);
 VLUA_DECLARE_UNSAFE(vifmview_cd);
 VLUA_DECLARE_SAFE(vifmview_entry);
+VLUA_DECLARE_UNSAFE(vifmview_select);
+VLUA_DECLARE_UNSAFE(vifmview_unselect);
 
 /* Methods of VifmView type. */
 static const luaL_Reg vifmview_methods[] = {
-	{ "cd",    VLUA_REF(vifmview_cd)    },
-	{ "entry", VLUA_REF(vifmview_entry) },
-	{ NULL,    NULL                     }
+	{ "cd",       VLUA_REF(vifmview_cd)       },
+	{ "entry",    VLUA_REF(vifmview_entry)    },
+	{ "select",   VLUA_REF(vifmview_select)   },
+	{ "unselect", VLUA_REF(vifmview_unselect) },
+	{ NULL,       NULL                        }
 };
 
 void
 vifmview_init(struct lua_State *lua)
 {
-	luaL_newmetatable(lua, "VifmView");
+	make_metatable(lua, "VifmView");
 	lua_pushcfunction(lua, VLUA_REF(vifmview_index));
 	lua_setfield(lua, -2, "__index");
 	luaL_setfuncs(lua, vifmview_methods, 0);
@@ -87,6 +99,22 @@ VLUA_API(vifmview_index)(lua_State *lua)
 	{
 		viewopts = 0;
 	}
+	else if(strcmp(key, "custom") == 0)
+	{
+		view_t *view = check_view(lua);
+		if(!flist_custom_active(view))
+		{
+			lua_pushnil(lua);
+			return 1;
+		}
+
+		lua_createtable(lua, /*narr=*/0, /*nrec=*/2);
+		lua_pushstring(lua, view->custom.title);
+		lua_setfield(lua, -2, "title");
+		lua_pushstring(lua, cv_describe(view->custom.type));
+		lua_setfield(lua, -2, "type");
+		return 1;
+	}
 	else if(strcmp(key, "cwd") == 0)
 	{
 		view_t *view = check_view(lua);
@@ -124,7 +152,7 @@ VLUA_API(vifmview_index)(lua_State *lua)
 	unsigned int *id_copy = lua_newuserdatauv(lua, sizeof(*id_copy), 0);
 	*id_copy = *id;
 
-	lua_newtable(lua);
+	make_metatable(lua, /*name=*/NULL);
 	lua_pushvalue(lua, -1);
 	lua_setmetatable(lua, -2);
 	lua_pushcfunction(lua,
@@ -303,8 +331,8 @@ VLUA_API(vifmview_otherview)(lua_State *lua)
 	return 1;
 }
 
-/* Method of `VifmView` that changes directory of current view.  Returns
- * boolean, which is true if location change was successful. */
+/* Method of `VifmView` that changes directory of a view.  Returns boolean,
+ * which is true if location change was successful. */
 static int
 VLUA_API(vifmview_cd)(lua_State *lua)
 {
@@ -334,6 +362,54 @@ VLUA_API(vifmview_entry)(lua_State *lua)
 	return 1;
 }
 
+/* Method of `VifmView` that selects entries a view.  Returns number of new
+ * selected entries. */
+static int
+VLUA_API(vifmview_select)(lua_State *lua)
+{
+	return select_unselect(lua, /*select=*/1);
+}
+
+/* Method of `VifmView` that unselects entries in a view.  Returns number of new
+ * unselected entries. */
+static int
+VLUA_API(vifmview_unselect)(lua_State *lua)
+{
+	return select_unselect(lua, /*select=*/0);
+}
+
+/* Selects or unselects entries in a view.  Returns number of entries that
+ * changed selection state. */
+static int
+select_unselect(lua_State *lua, int select)
+{
+	view_t *view = check_view(lua);
+
+	if(vle_mode_is(VISUAL_MODE) && !modvis_is_amending())
+	{
+		lua_pushinteger(lua, 0);
+		return 1;
+	}
+
+	int count = 0;
+	int *indexes = NULL;
+	if(extract_indexes(lua, view, &count, &indexes) != 0)
+	{
+		lua_pushinteger(lua, 0);
+		return 1;
+	}
+
+	const int was_selected = view->selected_files;
+
+	flist_sel_by_indexes(view, count, indexes, select);
+	free(indexes);
+
+	int num = select ? (view->selected_files - was_selected)
+	                 : (was_selected - view->selected_files);
+	lua_pushinteger(lua, num);
+	return 1;
+}
+
 /* Resolves `VifmView` user data in the first argument.  Returns the pointer or
  * aborts (Lua does longjmp()) if the view doesn't exist anymore. */
 static view_t *
diff --git a/src/lua/vlua.c b/src/lua/vlua.c
index 75ae4bf..37523db 100644
--- a/src/lua/vlua.c
+++ b/src/lua/vlua.c
@@ -23,131 +23,79 @@
 #include <stdlib.h> /* free() */
 #include <string.h> /* strdup() */
 
-#include "../cfg/config.h"
-#include "../cfg/info.h"
 #include "../compat/dtype.h"
 #include "../compat/fs_limits.h"
-#include "../engine/options.h"
-#include "../modes/dialogs/msg_dialog.h"
+#include "../engine/variables.h"
 #include "../ui/statusbar.h"
 #include "../ui/ui.h"
 #include "../utils/fs.h"
-#include "../utils/path.h"
 #include "../utils/str.h"
 #include "../utils/string_array.h"
-#include "../utils/utils.h"
 #include "../cmd_core.h"
-#include "../filelist.h"
-#include "../filename_modifiers.h"
 #include "../macros.h"
 #include "../plugins.h"
-#include "../running.h"
 #include "../status.h"
 #include "lua/lauxlib.h"
 #include "lua/lua.h"
 #include "api.h"
 #include "common.h"
+#include "vifm.h"
 #include "vifm_cmds.h"
+#include "vifm_events.h"
 #include "vifm_handlers.h"
-#include "vifm_keys.h"
-#include "vifm_tabs.h"
 #include "vifm_viewcolumns.h"
 #include "vifmjob.h"
-#include "vifmview.h"
+#include "vlua_cbacks.h"
 #include "vlua_state.h"
 
 static void patch_env(lua_State *lua);
 static void load_api(lua_State *lua);
-static int VLUA_API(api_is_at_least)(lua_State *lua);
-static int VLUA_API(api_has)(lua_State *lua);
 static int VLUA_API(print)(lua_State *lua);
-static int VLUA_API(opts_global_index)(lua_State *lua);
-static int VLUA_API(opts_global_newindex)(lua_State *lua);
-static int VLUA_API(vifm_errordialog)(lua_State *lua);
-static int VLUA_API(vifm_fnamemodify)(lua_State *lua);
-static int VLUA_API(vifm_escape)(lua_State *lua);
-static int VLUA_API(vifm_exists)(lua_State *lua);
-static int VLUA_API(vifm_makepath)(lua_State *lua);
-static int VLUA_API(vifm_expand)(lua_State *lua);
-static int VLUA_API(vifm_run)(lua_State *lua);
-static int VLUA_API(vifm_sessions_current)(lua_State *lua);
+static int VLUA_API(os_getenv)(lua_State *lua);
 static int VLUA_API(vifm_plugin_require)(lua_State *lua);
 static int VLUA_IMPL(require_plugin_module)(lua_State *lua);
-static int VLUA_API(sb_info)(lua_State *lua);
-static int VLUA_API(sb_error)(lua_State *lua);
-static int VLUA_API(sb_quick)(lua_State *lua);
-static int load_plugin(lua_State *lua, const char name[], plug_t *plug);
+static int load_plugin(lua_State *lua, plug_t *plug);
 static void setup_plugin_env(lua_State *lua, plug_t *plug);
 
-VLUA_DECLARE_SAFE(api_is_at_least);
-VLUA_DECLARE_SAFE(api_has);
 VLUA_DECLARE_SAFE(print);
-VLUA_DECLARE_SAFE(opts_global_index);
-VLUA_DECLARE_UNSAFE(opts_global_newindex);
-VLUA_DECLARE_SAFE(vifm_errordialog);
-VLUA_DECLARE_SAFE(vifm_fnamemodify);
-VLUA_DECLARE_SAFE(vifm_escape);
-VLUA_DECLARE_SAFE(vifm_exists);
-VLUA_DECLARE_SAFE(vifm_makepath);
-VLUA_DECLARE_SAFE(vifm_expand);
-VLUA_DECLARE_SAFE(vifm_run);
-VLUA_DECLARE_SAFE(vifm_sessions_current);
+VLUA_DECLARE_SAFE(os_getenv);
 VLUA_DECLARE_UNSAFE(vifm_plugin_require);
-VLUA_DECLARE_SAFE(sb_info);
-VLUA_DECLARE_SAFE(sb_error);
-VLUA_DECLARE_SAFE(sb_quick);
-
-/* These are defined in other units. */
-VLUA_DECLARE_SAFE(vifmjob_new);
-VLUA_DECLARE_SAFE(vifmview_currview);
-VLUA_DECLARE_SAFE(vifmview_otherview);
-VLUA_DECLARE_UNSAFE(vifm_addcolumntype);
+
+/* Defined in another unit. */
 VLUA_DECLARE_SAFE(vifm_addhandler);
 
-/* Functions of `vifm` global table. */
-static const struct luaL_Reg vifm_methods[] = {
-	{ "errordialog",   VLUA_REF(vifm_errordialog)   },
-	{ "fnamemodify",   VLUA_REF(vifm_fnamemodify)   },
-	{ "escape",        VLUA_REF(vifm_escape)        },
-	{ "exists",        VLUA_REF(vifm_exists)        },
-	{ "makepath",      VLUA_REF(vifm_makepath)      },
-	{ "startjob",      VLUA_REF(vifmjob_new)        },
-	{ "expand",        VLUA_REF(vifm_expand)        },
-	{ "currview",      VLUA_REF(vifmview_currview)  },
-	{ "otherview",     VLUA_REF(vifmview_otherview) },
-	{ "addcolumntype", VLUA_REF(vifm_addcolumntype) },
-	{ "addhandler",    VLUA_REF(vifm_addhandler)    },
-	{ "run",           VLUA_REF(vifm_run)           },
-	{ NULL,            NULL                         }
-};
-
-/* Functions of `vifm.sb` table. */
-static const struct luaL_Reg sb_methods[] = {
-	{ "info",   VLUA_REF(sb_info)  },
-	{ "error",  VLUA_REF(sb_error) },
-	{ "quick",  VLUA_REF(sb_quick) },
-	{ NULL,     NULL               }
-};
+/* Address of this variable serves as a key in Lua table. */
+static char plugin_envs;
 
 vlua_t *
 vlua_init(void)
 {
 	vlua_t *vlua = vlua_state_alloc();
-	if(vlua != NULL)
+	if(vlua == NULL)
 	{
-		patch_env(vlua->lua);
-		load_api(vlua->lua);
-
-		vifm_viewcolumns_init(vlua);
-		vifm_handlers_init(vlua);
+		return NULL;
 	}
+
+	patch_env(vlua->lua);
+	load_api(vlua->lua);
+
+	vlua_cbacks_init(vlua);
+	vifm_viewcolumns_init(vlua);
+	vifm_handlers_init(vlua);
+
+	vlua_state_make_table(vlua, &plugin_envs);
+
 	return vlua;
 }
 
 void
 vlua_finish(vlua_t *vlua)
 {
-	vlua_state_free(vlua);
+	if(vlua != NULL)
+	{
+		vifmjob_finish(vlua->lua);
+		vlua_state_free(vlua);
+	}
 }
 
 /* Adjusts standard libraries. */
@@ -158,13 +106,15 @@ patch_env(lua_State *lua)
 	lua_setglobal(lua, "print");
 
 	lua_getglobal(lua, "os");
-	lua_newtable(lua);
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/6);
 	lua_getfield(lua, -2, "clock");
 	lua_setfield(lua, -2, "clock");
 	lua_getfield(lua, -2, "date");
 	lua_setfield(lua, -2, "date");
 	lua_getfield(lua, -2, "difftime");
 	lua_setfield(lua, -2, "difftime");
+	lua_pushcfunction(lua, VLUA_REF(os_getenv));
+	lua_setfield(lua, -2, "getenv");
 	lua_getfield(lua, -2, "time");
 	lua_setfield(lua, -2, "time");
 	lua_getfield(lua, -2, "tmpname");
@@ -177,103 +127,13 @@ patch_env(lua_State *lua)
 static void
 load_api(lua_State *lua)
 {
-	vifmjob_init(lua);
-	vifmview_init(lua);
-
-	luaL_newmetatable(lua, "VifmPluginEnv");
+	make_metatable(lua, "VifmPluginEnv");
 	lua_pushglobaltable(lua);
 	lua_setfield(lua, -2, "__index");
 	lua_pop(lua, 1);
 
-	luaL_newlib(lua, vifm_methods);
-
-	lua_pushvalue(lua, -1);
+	vifm_init(lua);
 	lua_setglobal(lua, "vifm");
-
-	/* Setup vifm.cmds. */
-	vifm_cmds_init(lua);
-	lua_setfield(lua, -2, "cmds");
-
-	/* Setup vifm.keys. */
-	vifm_keys_init(lua);
-	lua_setfield(lua, -2, "keys");
-
-	/* Setup vifm.tabs. */
-	vifm_tabs_init(lua);
-	lua_setfield(lua, -2, "tabs");
-
-	/* Setup vifm.opts. */
-	lua_newtable(lua);
-	lua_pushvalue(lua, -1);
-	lua_setfield(lua, -3, "opts");
-	lua_newtable(lua);
-	lua_newtable(lua);
-	lua_pushcfunction(lua, VLUA_REF(opts_global_index));
-	lua_setfield(lua, -2, "__index");
-	lua_pushcfunction(lua, VLUA_REF(opts_global_newindex));
-	lua_setfield(lua, -2, "__newindex");
-	lua_setmetatable(lua, -2);
-	lua_setfield(lua, -2, "global");
-	lua_pop(lua, 1);
-
-	/* Setup vifm.plugins. */
-	lua_newtable(lua);
-	lua_newtable(lua);
-	lua_setfield(lua, -2, "all");
-	lua_setfield(lua, -2, "plugins");
-
-	/* Setup vifm.version. */
-	lua_newtable(lua);
-	lua_newtable(lua);
-	lua_pushstring(lua, VERSION);
-	lua_setfield(lua, -2, "str");
-	lua_setfield(lua, -2, "app");
-	lua_newtable(lua);
-	lua_pushinteger(lua, 0);
-	lua_setfield(lua, -2, "major");
-	lua_pushinteger(lua, 0);
-	lua_setfield(lua, -2, "minor");
-	lua_pushinteger(lua, 0);
-	lua_setfield(lua, -2, "patch");
-	lua_pushcfunction(lua, VLUA_REF(api_has));
-	lua_setfield(lua, -2, "has");
-	lua_pushcfunction(lua, VLUA_REF(api_is_at_least));
-	lua_setfield(lua, -2, "atleast");
-	lua_setfield(lua, -2, "api");
-	lua_setfield(lua, -2, "version");
-
-	/* Setup vifm.sb. */
-	luaL_newlib(lua, sb_methods);
-	lua_setfield(lua, -2, "sb");
-
-	/* Setup vifm.sessions. */
-	lua_newtable(lua);
-	lua_pushcfunction(lua, VLUA_REF(vifm_sessions_current));
-	lua_setfield(lua, -2, "current");
-	lua_setfield(lua, -2, "sessions");
-
-	/* vifm. */
-	lua_pop(lua, 1);
-}
-
-/* Checks version of the API.  Returns a boolean. */
-static int
-VLUA_API(api_is_at_least)(lua_State *lua)
-{
-	const int major = luaL_checkinteger(lua, 1);
-	const int minor = luaL_optinteger(lua, 2, 0);
-	const int patch = luaL_optinteger(lua, 3, 0);
-	lua_pushboolean(lua, (major == 0 && minor == 0 && patch == 0));
-	return 1;
-}
-
-/* Performs tests for API features.  Returns a boolean. */
-static int
-VLUA_API(api_has)(lua_State *lua)
-{
-	(void)luaL_checkstring(lua, 1);
-	lua_pushboolean(lua, 0);
-	return 1;
 }
 
 /* Replacement of standard global `print` function.  If plugin is present,
@@ -313,189 +173,24 @@ VLUA_API(print)(lua_State *lua)
 	return 0;
 }
 
-/* Provides read access to global options by their name as
- * `vifm.opts.global[name]`. */
+/* os.getenv() that's aware of Vifm's internal variables. */
 static int
-VLUA_API(opts_global_index)(lua_State *lua)
+VLUA_API(os_getenv)(lua_State *lua)
 {
-	const char *opt_name = luaL_checkstring(lua, 2);
-
-	opt_t *opt = vle_opts_find(opt_name, OPT_ANY);
-	if(opt == NULL || opt->scope == OPT_LOCAL)
-	{
-		return 0;
-	}
-
-	return get_opt(lua, opt);
-}
-
-/* Provides write access to global options by their name as
- * `vifm.opts.global[name] = value`. */
-static int
-VLUA_API(opts_global_newindex)(lua_State *lua)
-{
-	const char *opt_name = luaL_checkstring(lua, 2);
-
-	opt_t *opt = vle_opts_find(opt_name, OPT_ANY);
-	if(opt == NULL || opt->scope == OPT_LOCAL)
-	{
-		return 0;
-	}
-
-	return set_opt(lua, opt);
-}
-
-/* Member of `vifm` that displays an error dialog.  Doesn't return anything. */
-static int
-VLUA_API(vifm_errordialog)(lua_State *lua)
-{
-	const char *title = luaL_checkstring(lua, 1);
-	const char *msg = luaL_checkstring(lua, 2);
-	show_error_msg(title, msg);
-	return 0;
-}
-
-/* Member of `vifm` that modifies path according to specifiers.  Returns
- * modified path. */
-static int
-VLUA_API(vifm_fnamemodify)(lua_State *lua)
-{
-	const char *path = luaL_checkstring(lua, 1);
-	const char *modifiers = luaL_checkstring(lua, 2);
-	const char *base = luaL_optstring(lua, 3, flist_get_dir(curr_view));
-	lua_pushstring(lua, mods_apply(path, base, modifiers, 0));
-	return 1;
-}
-
-/* Member of `vifm` that escapes its input string.  Returns escaped string. */
-static int
-VLUA_API(vifm_escape)(lua_State *lua)
-{
-	const char *what = luaL_checkstring(lua, 1);
-	lua_pushstring(lua, enclose_in_dquotes(what, curr_stats.shell_type));
-	return 1;
-}
-
-/* Member of `vifm` that checks whether specified path exists without resolving
- * symbolic links.  Returns a boolean, which is true when path does exist. */
-static int
-VLUA_API(vifm_exists)(lua_State *lua)
-{
-	const char *path = luaL_checkstring(lua, 1);
-	lua_pushboolean(lua, path_exists(path, NODEREF));
+	lua_pushstring(lua, local_getenv_null(luaL_checkstring(lua, 1)));
 	return 1;
 }
 
-/* Member of `vifm` that creates a directory and all of its missing parent
- * directories.  Returns a boolean, which is true on success. */
-static int
-VLUA_API(vifm_makepath)(lua_State *lua)
-{
-	const char *path = luaL_checkstring(lua, 1);
-	lua_pushboolean(lua, make_path(path, 0755) == 0);
-	return 1;
-}
-
-/* Member of `vifm` that expands macros and environment variables.  Returns the
- * expanded string. */
-static int
-VLUA_API(vifm_expand)(lua_State *lua)
-{
-	const char *str = luaL_checkstring(lua, 1);
-
-	char *env_expanded = expand_envvars(str,
-			EEF_KEEP_ESCAPES | EEF_DOUBLE_PERCENTS);
-	char *full_expanded = ma_expand(env_expanded, NULL, NULL, MER_DISPLAY);
-	lua_pushstring(lua, full_expanded);
-	free(env_expanded);
-	free(full_expanded);
-
-	return 1;
-}
-
-/* Runs an external command similar to :!. */
-static int
-VLUA_API(vifm_run)(lua_State *lua)
-{
-	luaL_checktype(lua, 1, LUA_TTABLE);
-
-	check_field(lua, 1, "cmd", LUA_TSTRING);
-	const char *cmd = lua_tostring(lua, -1);
-
-	int use_term_mux = 1;
-	if(check_opt_field(lua, 1, "usetermmux", LUA_TBOOLEAN))
-	{
-		use_term_mux = lua_toboolean(lua, -1);
-	}
-
-	ShellPause pause = PAUSE_ON_ERROR;
-	if(check_opt_field(lua, 1, "pause", LUA_TSTRING))
-	{
-		const char *value = lua_tostring(lua, -1);
-		if(strcmp(value, "never") == 0)
-		{
-			pause = PAUSE_NEVER;
-		}
-		else if(strcmp(value, "onerror") == 0)
-		{
-			pause = PAUSE_ON_ERROR;
-		}
-		else if(strcmp(value, "always") == 0)
-		{
-			pause = PAUSE_ALWAYS;
-		}
-		else
-		{
-			return luaL_error(lua, "Unrecognized value for `pause`: %s", value);
-		}
-	}
-
-	lua_pushinteger(lua, rn_shell(cmd, pause, use_term_mux, SHELL_BY_APP));
-	return 1;
-}
-
-/* Member of `vifm.sb` that prints a normal message on the status bar.  Doesn't
- * return anything. */
-static int
-VLUA_API(sb_info)(lua_State *lua)
-{
-	const char *msg = luaL_checkstring(lua, 1);
-	ui_sb_msg(msg);
-	curr_stats.save_msg = 1;
-	return 0;
-}
-
-/* Member of `vifm.sb` that prints an error message on the status bar.  Doesn't
- * return anything. */
-static int
-VLUA_API(sb_error)(lua_State *lua)
-{
-	const char *msg = luaL_checkstring(lua, 1);
-	ui_sb_err(msg);
-	curr_stats.save_msg = 1;
-	return 0;
-}
-
-/* Member of `vifm.sb` that prints status bar message that's not stored in
- * history.  Doesn't return anything. */
-static int
-VLUA_API(sb_quick)(lua_State *lua)
-{
-	const char *msg = luaL_checkstring(lua, 1);
-	ui_sb_quick_msgf("%s", msg);
-	return 0;
-}
-
 int
-vlua_load_plugin(vlua_t *vlua, const char plugin[], plug_t *plug)
+vlua_load_plugin(vlua_t *vlua, plug_t *plug)
 {
-	if(load_plugin(vlua->lua, plugin, plug) == 0)
+	if(load_plugin(vlua->lua, plug) == 0)
 	{
 		lua_getglobal(vlua->lua, "vifm");
 		lua_getfield(vlua->lua, -1, "plugins");
 		lua_getfield(vlua->lua, -1, "all");
 		lua_pushvalue(vlua->lua, -4);
-		lua_setfield(vlua->lua, -2, plugin);
+		lua_setfield(vlua->lua, -2, plug->name);
 		lua_pop(vlua->lua, 4);
 		return 0;
 	}
@@ -506,17 +201,16 @@ vlua_load_plugin(vlua_t *vlua, const char plugin[], plug_t *plug)
  * that corresponds to the module onto the stack, otherwise non-zero is
  * returned. */
 static int
-load_plugin(lua_State *lua, const char name[], plug_t *plug)
+load_plugin(lua_State *lua, plug_t *plug)
 {
 	char full_path[PATH_MAX + 32];
-	snprintf(full_path, sizeof(full_path), "%s/plugins/%s/init.lua",
-			cfg.config_dir, name);
+	snprintf(full_path, sizeof(full_path), "%s/init.lua", plug->path);
 
 	if(luaL_loadfile(lua, full_path) != LUA_OK)
 	{
 		const char *error = lua_tostring(lua, -1);
 		plug_log(plug, error);
-		ui_sb_errf("Failed to load '%s' plugin: %s", name, error);
+		ui_sb_errf("Failed to load '%s' plugin: %s", plug->name, error);
 		lua_pop(lua, 1);
 		return 1;
 	}
@@ -526,14 +220,14 @@ load_plugin(lua_State *lua, const char name[], plug_t *plug)
 	{
 		const char *error = lua_tostring(lua, -1);
 		plug_log(plug, error);
-		ui_sb_errf("Failed to start '%s' plugin: %s", name, error);
+		ui_sb_errf("Failed to start '%s' plugin: %s", plug->name, error);
 		lua_pop(lua, 1);
 		return 1;
 	}
 
 	if(lua_gettop(lua) == 0 || !lua_istable(lua, -1))
 	{
-		ui_sb_errf("Failed to load '%s' plugin: %s", name,
+		ui_sb_errf("Failed to load '%s' plugin: %s", plug->name,
 				"it didn't return a table");
 		if(lua_gettop(lua) > 0)
 		{
@@ -550,20 +244,23 @@ static void
 setup_plugin_env(lua_State *lua, plug_t *plug)
 {
 	/* Global environment table. */
-	lua_newtable(lua);
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/1);
 	luaL_getmetatable(lua, "VifmPluginEnv");
 	lua_setmetatable(lua, -2);
+	/* Don't let a plugin access true global table by using _G explicitly. */
+	lua_pushvalue(lua, -1);
+	lua_setfield(lua, -2, "_G");
 
 	/* Plugin-specific `vifm` table. */
-	lua_newtable(lua);
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/2);
 	/* Meta-table for it. */
-	lua_newtable(lua);
+	make_metatable(lua, /*name=*/NULL);
 	lua_getglobal(lua, "vifm");
 	lua_setfield(lua, -2, "__index");
 	lua_setmetatable(lua, -2);
 
 	/* Plugin-specific `vifm.plugin` table. */
-	lua_newtable(lua);
+	lua_createtable(lua, /*narr=*/0, /*nrec=*/3);
 	lua_pushstring(lua, plug->name);
 	lua_setfield(lua, -2, "name");
 	lua_pushstring(lua, plug->path);
@@ -572,12 +269,12 @@ setup_plugin_env(lua_State *lua, plug_t *plug)
 	lua_pushlightuserdata(lua, plug);
 	lua_pushcclosure(lua, VLUA_REF(vifm_plugin_require), 1);
 	lua_setfield(lua, -2, "require");
-	lua_setfield(lua, -2, "plugin");
+	lua_setfield(lua, -2, "plugin");  /* vifm.plugin */
 
 	/* Plugin-specific `vifm.addhandler()`. */
 	lua_pushlightuserdata(lua, plug);
 	lua_pushcclosure(lua, VLUA_REF(vifm_addhandler), 1);
-	lua_setfield(lua, -2, "addhandler");
+	lua_setfield(lua, -2, "addhandler"); /* vifm.addhandler */
 
 	/* Assign `vifm` as a plugin-specific global. */
 	lua_setfield(lua, -2, "vifm");
@@ -587,28 +284,19 @@ setup_plugin_env(lua_State *lua, plug_t *plug)
 	lua_pushcclosure(lua, VLUA_REF(print), 1);
 	lua_setfield(lua, -2, "print");
 
+	/* Map plug to plugin environment for future queries. */
+	vlua_state_get_table(get_state(lua), &plugin_envs);
+	lua_pushlightuserdata(lua, plug);
+	lua_pushvalue(lua, -3);
+	lua_settable(lua, -3);
+	lua_pop(lua, 1);
+
 	if(lua_setupvalue(lua, -2, 1) == NULL)
 	{
 		lua_pop(lua, 1);
 	}
 }
 
-/* Member of `vifm.sessions` that retrieves name of the current session.
- * Returns string or nil. */
-static int
-VLUA_API(vifm_sessions_current)(lua_State *lua)
-{
-	if(sessions_active())
-	{
-		lua_pushstring(lua, sessions_current());
-	}
-	else
-	{
-		lua_pushnil(lua);
-	}
-	return 1;
-}
-
 /* Member of `vifm.plugin` that loads a module relative to the plugin's root.
  * Returns module's return. */
 static int
@@ -619,7 +307,7 @@ VLUA_API(vifm_plugin_require)(lua_State *lua)
 	plug_t *plug = lua_touserdata(lua, lua_upvalueindex(1));
 	if(plug == NULL)
 	{
-		assert(false && "vifm.plugin.require() called outside a plugin?");
+		assert(0 && "vifm.plugin.require() called outside a plugin?");
 		return 0;
 	}
 
@@ -631,7 +319,21 @@ VLUA_API(vifm_plugin_require)(lua_State *lua)
 				mod_name);
 	}
 
-	luaL_requiref(lua, full_path, VLUA_IREF(require_plugin_module), 1);
+	/* luaL_requiref() equivalent that passes plug to require_plugin_module(). */
+	luaL_getsubtable(lua, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);
+	lua_getfield(lua, -1, full_path);
+	if(!lua_toboolean(lua, -1))
+	{
+		lua_pop(lua, 1);
+		lua_pushlightuserdata(lua, plug);
+		lua_pushcclosure(lua, VLUA_IREF(require_plugin_module), 1);
+		lua_pushstring(lua, full_path);
+		lua_call(lua, 1, 1);
+		lua_pushvalue(lua, -1);
+		lua_setfield(lua, -3, full_path);
+	}
+	lua_remove(lua, -2);
+
 	return 1;
 }
 
@@ -640,7 +342,32 @@ static int
 VLUA_IMPL(require_plugin_module)(lua_State *lua)
 {
 	const char *mod = luaL_checkstring(lua, 1);
-	if(luaL_loadfile(lua, mod) != LUA_OK || lua_pcall(lua, 0, 1, 0) != LUA_OK)
+	if(luaL_loadfile(lua, mod) != LUA_OK)
+	{
+		const char *error = lua_tostring(lua, -1);
+		return luaL_error(lua, "vifm.plugin.require('%s'): %s", mod, error);
+	}
+
+	/* Fetch custom environment of the current plugin. */
+	plug_t *plug = lua_touserdata(lua, lua_upvalueindex(1));
+	assert(plug != NULL && "Invalid call to require_plugin_module()");
+	vlua_state_get_table(get_state(lua), &plugin_envs);
+	lua_pushlightuserdata(lua, plug);
+	if(lua_gettable(lua, -2) != LUA_TTABLE)
+	{
+		return luaL_error(lua,
+				"vifm.plugin.require('%s'): failed to fetch plugin env", mod);
+	}
+	lua_remove(lua, -2);
+
+	/* Use that environment for the newly loaded module. */
+	if(lua_setupvalue(lua, -2, 1) == NULL)
+	{
+		return luaL_error(lua,
+				"vifm.plugin.require('%s'): failed to copy plugin env", mod);
+	}
+
+	if(lua_pcall(lua, 0, 1, 0) != LUA_OK)
 	{
 		const char *error = lua_tostring(lua, -1);
 		return luaL_error(lua, "vifm.plugin.require('%s'): %s", mod, error);
@@ -701,17 +428,24 @@ vlua_view_file(vlua_t *vlua, const char viewer[], const char path[],
 }
 
 void
-vlua_open_file(vlua_t *vlua, const char prog[], const dir_entry_t *entry)
+vlua_open_file(vlua_t *vlua, const char prog[], const struct dir_entry_t *entry)
 {
 	return vifm_handlers_open(vlua, prog, entry);
 }
 
 char *
-vlua_make_status_line(vlua_t *vlua, const char format[], view_t *view, int width)
+vlua_make_status_line(vlua_t *vlua, const char format[], struct view_t *view,
+		int width)
 {
 	return vifm_handlers_make_status_line(vlua, format, view, width);
 }
 
+char *
+vlua_make_tab_line(vlua_t *vlua, const char format[], int other, int width)
+{
+	return vifm_handlers_make_tab_line(vlua, format, other, width);
+}
+
 int
 vlua_open_help(vlua_t *vlua, const char handler[], const char topic[])
 {
@@ -739,5 +473,30 @@ vlua_edit_list(vlua_t *vlua, const char handler[], char *entries[],
 			quickfix_format);
 }
 
+void
+vlua_process_callbacks(vlua_t *vlua)
+{
+	vlua_cbacks_process(vlua);
+}
+
+void
+vlua_events_app_exit(vlua_t *vlua)
+{
+	if(vlua != NULL)
+	{
+		vifm_events_app_exit(vlua);
+	}
+}
+
+void
+vlua_events_app_fsop(vlua_t *vlua, OPS op, const char path[],
+		const char target[], void *extra, int dir)
+{
+	if(vlua != NULL)
+	{
+		vifm_events_app_fsop(vlua, op, path, target, extra, dir);
+	}
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 : */
diff --git a/src/lua/vlua.h b/src/lua/vlua.h
index 1134675..df38dfd 100644
--- a/src/lua/vlua.h
+++ b/src/lua/vlua.h
@@ -19,6 +19,8 @@
 #ifndef VIFM__LUA__VLUA_H__
 #define VIFM__LUA__VLUA_H__
 
+#include "../ops.h"
+
 /* This unit implements Lua interface.  It provides API for the rest of the
  * application and thus this is the only header that needs to be included from
  * the outside. */
@@ -37,7 +39,7 @@ struct view_t;
 vlua_t * vlua_init(void);
 
 /* Loads a single plugin on request.  Returns zero on success. */
-int vlua_load_plugin(vlua_t *vlua, const char plugin[], struct plug_t *plug);
+int vlua_load_plugin(vlua_t *vlua, struct plug_t *plug);
 
 /* Frees resources of the unit.  The parameter can be NULL. */
 void vlua_finish(vlua_t *vlua);
@@ -46,10 +48,14 @@ void vlua_finish(vlua_t *vlua);
  * returned. */
 int vlua_run_string(vlua_t *vlua, const char str[]);
 
+/* Command-line commands. */
+
 /* Performs completion of a command.  Returns offset of completion matches. */
 int vlua_complete_cmd(vlua_t *vlua, const struct cmd_info_t *cmd_info,
 		int arg_pos);
 
+/* View columns. */
+
 /* Maps column name to column id.  Returns column id or -1 on error. */
 int vlua_viewcolumn_map(vlua_t *vlua, const char name[]);
 
@@ -57,6 +63,8 @@ int vlua_viewcolumn_map(vlua_t *vlua, const char name[]);
  * Returns non-zero if so, otherwise zero is returned. */
 int vlua_viewcolumn_is_primary(vlua_t *vlua, int column_id);
 
+/* Handlers. */
+
 /* Checks command for a Lua handler.  Returns non-zero if it's present and zero
  * otherwise. */
 int vlua_handler_cmd(vlua_t *vlua, const char cmd[]);
@@ -79,6 +87,13 @@ void vlua_open_file(vlua_t *vlua, const char prog[],
 char * vlua_make_status_line(struct vlua_t *vlua, const char format[],
 		struct view_t *view, int width);
 
+/* Invokes tab line formatting handler.  Returns newly allocated string with
+ * tab line format. */
+char * vlua_make_tab_line(vlua_t *vlua, const char format[], int other,
+		int width);
+
+/* Operations with editor. */
+
 /* Invokes an editor handler to view Vim-style documentation.  Returns zero on
  * success and non-zero otherwise. */
 int vlua_open_help(struct vlua_t *vlua, const char handler[],
@@ -101,6 +116,22 @@ int vlua_edit_many(struct vlua_t *vlua, const char handler[], char *files[],
 int vlua_edit_list(struct vlua_t *vlua, const char handler[], char *entries[],
 		int nentries, int current, int quickfix_format);
 
+/* Callbacks. */
+
+/* Processes all callbacks accumulated so far in the queue. */
+void vlua_process_callbacks(struct vlua_t *vlua);
+
+/* Events. */
+
+/* Schedules all handlers for exit event as callbacks to process.  The vlua
+ * parameter can be NULL. */
+void vlua_events_app_exit(struct vlua_t *vlua);
+
+/* Schedules all handlers for fsop event as callbacks.  The vlua, target and
+ * extra parameters can be NULL. */
+void vlua_events_app_fsop(struct vlua_t *vlua, OPS op, const char path[],
+		const char target[], void *extra, int dir);
+
 #endif /* VIFM__LUA__VLUA_H__ */
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/src/lua/vlua_cbacks.c b/src/lua/vlua_cbacks.c
new file mode 100644
index 0000000..56111ae
--- /dev/null
+++ b/src/lua/vlua_cbacks.c
@@ -0,0 +1,110 @@
+/* vifm
+ * Copyright (C) 2022 xaizek.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+#include "vlua_cbacks.h"
+
+#include "../ui/statusbar.h"
+#include "lua/lua.h"
+#include "vlua_state.h"
+
+/* Address of this variable serves as a key in Lua table. */
+static char cbacks_queue;
+
+void
+vlua_cbacks_init(vlua_t *vlua)
+{
+	vlua_state_make_table(vlua, &cbacks_queue);
+}
+
+void
+vlua_cbacks_process(vlua_t *vlua)
+{
+	vlua_state_get_table(vlua, &cbacks_queue);
+
+	/* Replace with an empty table, so there are no updates to the table while
+	 * it's being traversed. */
+	vlua_state_make_table(vlua, &cbacks_queue);
+
+	lua_len(vlua->lua, -1);
+	int len = lua_tointeger(vlua->lua, -1);
+	lua_pop(vlua->lua, 1);
+
+	int i;
+	for(i = 0; i < len; ++i)
+	{
+		lua_geti(vlua->lua, -1, 1 + i);
+
+		lua_getfield(vlua->lua, -1, "handler");
+
+		lua_getfield(vlua->lua, -2, "argv");
+		lua_len(vlua->lua, -1);
+		int argc = lua_tointeger(vlua->lua, -1);
+		lua_pop(vlua->lua, 1);
+
+		int j;
+		for(j = 0; j < argc; ++j)
+		{
+			lua_geti(vlua->lua, -1 - j, 1 + j);
+		}
+
+		lua_remove(vlua->lua, -1 - argc);
+
+		if(lua_pcall(vlua->lua, argc, 0, 0) != LUA_OK)
+		{
+			const char *error = lua_tostring(vlua->lua, -1);
+			ui_sb_err(error);
+			lua_pop(vlua->lua, 1);
+		}
+
+		lua_pop(vlua->lua, 1);
+	}
+
+	lua_pop(vlua->lua, 1);
+}
+
+void
+vlua_cbacks_schedule(vlua_t *vlua, int argc)
+{
+	lua_createtable(vlua->lua, /*narr=*/0, /*nrec=*/0); /* scheduled table */
+
+	lua_createtable(vlua->lua, argc, /*nrec=*/0);
+	int i;
+	for(i = 0; i < argc; ++i)
+	{
+		lua_rotate(vlua->lua, -3 - (argc - 1 - i), -1);
+		lua_seti(vlua->lua, -2, 1 + i);
+	}
+	lua_setfield(vlua->lua, -2, "argv");
+
+	lua_rotate(vlua->lua, -2, 1);
+	lua_setfield(vlua->lua, -2, "handler");
+
+	vlua_state_get_table(vlua, &cbacks_queue); /* cbacks table */
+
+	lua_len(vlua->lua, -1); /* of cbacks table */
+	int len = lua_tointeger(vlua->lua, -1);
+	lua_pop(vlua->lua, 1); /* length */
+
+	lua_rotate(vlua->lua, -2, 1); /* swap cbacks table and scheduled table */
+	lua_seti(vlua->lua, -2, 1 + len);
+
+	lua_pop(vlua->lua, 1); /* cbacks table */
+}
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 : */
diff --git a/src/lua/vlua_cbacks.h b/src/lua/vlua_cbacks.h
new file mode 100644
index 0000000..e3538c1
--- /dev/null
+++ b/src/lua/vlua_cbacks.h
@@ -0,0 +1,40 @@
+/* vifm
+ * Copyright (C) 2022 xaizek.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+#ifndef VIFM__LUA__VLUA_CBACKS_H__
+#define VIFM__LUA__VLUA_CBACKS_H__
+
+/* This unit manages queue of callbacks that are to be processed when it's safe
+ * to do so. */
+
+struct vlua_t;
+
+/* Makes usage of this unit possible. */
+void vlua_cbacks_init(struct vlua_t *vlua);
+
+/* Process all queued callbacks so far. */
+void vlua_cbacks_process(struct vlua_t *vlua);
+
+/* Take argc number of parameters and then function from the Lua stack and push
+ * them to queue of callbacks. */
+void vlua_cbacks_schedule(struct vlua_t *vlua, int argc);
+
+#endif /* VIFM__LUA__VLUA_CBACKS_H__ */
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 : */
diff --git a/src/macros.c b/src/macros.c
index fb07535..c1a7413 100644
--- a/src/macros.c
+++ b/src/macros.c
@@ -149,7 +149,7 @@ filter_single(int *quoted, char c, char data, int ncurr, int nother)
 
 	if(c == 'r')
 	{
-		reg_t *reg = regs_find(tolower(data));
+		const reg_t *reg = regs_find(tolower(data));
 		if(reg != NULL && reg->nfiles == 1)
 		{
 			return c;
@@ -295,8 +295,9 @@ expand_macros_i(const char command[], const char args[], MacroFlags *flags,
 			case 'n': /* Forbid using of terminal multiplexer, even if active. */
 				ma_flags_set(flags, MF_NO_TERM_MUX);
 				break;
-			case 'N': /* Do not run command in a separate terminal session. */
-				ma_flags_set(flags, MF_KEEP_SESSION);
+			case 'N': /* Do not run the command in a separate terminal session or
+			             process group. */
+				ma_flags_set(flags, MF_KEEP_IN_FG);
 				break;
 			case 'm': /* Use menu. */
 				ma_flags_set(flags, MF_MENU_OUTPUT);
@@ -560,11 +561,8 @@ static char *
 expand_register(const char curr_dir[], char expanded[], int quotes,
 		const char mod[], int key, int *well_formed, int for_shell)
 {
-	int i;
-	reg_t *reg;
-
 	*well_formed = 1;
-	reg = regs_find(tolower(key));
+	const reg_t *reg = regs_find(tolower(key));
 	if(reg == NULL)
 	{
 		*well_formed = 0;
@@ -573,6 +571,7 @@ expand_register(const char curr_dir[], char expanded[], int quotes,
 		mod--;
 	}
 
+	int i;
 	for(i = 0; i < reg->nfiles; ++i)
 	{
 		const char *const modified = mods_apply(reg->files[i], curr_dir, mod,
@@ -674,7 +673,7 @@ append_path_to_expanded(char expanded[], int quotes, const char path[])
 	}
 	else
 	{
-		char *const escaped = shell_like_escape(path, 0);
+		char *const escaped = posix_like_escape(path, /*type=*/0);
 		if(escaped == NULL)
 		{
 			show_error_msg("Memory Error", "Unable to allocate enough memory");
@@ -765,14 +764,21 @@ expand_custom(const char **pattern, size_t nmacros, custom_macro_t macros[],
 		}
 		else if(pat[1] == '*')
 		{
-			cline_set_attr(&result, '0');
+			cline_set_attr(&result, /*user_color=*/0);
 			++*pattern;
 		}
 		else if(isdigit(pat[1]) && pat[2] == '*')
 		{
-			cline_set_attr(&result, pat[1]);
+			int user_color = pat[1] - '0';
+			cline_set_attr(&result, user_color);
 			*pattern += 2;
 		}
+		else if(isdigit(pat[1]) && isdigit(pat[2]) && pat[3] == '*')
+		{
+			int user_color = (pat[1] - '0')*10 + (pat[2] - '0');
+			cline_set_attr(&result, user_color);
+			*pattern += 3;
+		}
 		else if(with_opt && pat[1] == '[')
 		{
 			++*pattern;
@@ -929,7 +935,7 @@ ma_flags_to_str(MacroFlags flags)
 		case MF_IGNORE: return "%i";
 		case MF_NO_TERM_MUX: return "%n";
 
-		case MF_KEEP_SESSION: return "%N";
+		case MF_KEEP_IN_FG: return "%N";
 
 		case MF_PIPE_FILE_LIST: return "%Pl";
 		case MF_PIPE_FILE_LIST_Z: return "%Pz";
diff --git a/src/macros.h b/src/macros.h
index c26e97a..5c10333 100644
--- a/src/macros.h
+++ b/src/macros.h
@@ -50,7 +50,8 @@ typedef enum
 	MF_IGNORE,      /* Completely ignore command output. */
 	MF_NO_TERM_MUX, /* Forbid using terminal multiplexer, even if active. */
 
-	MF_KEEP_SESSION, /* Don't detach command from terminal session. */
+	MF_KEEP_IN_FG, /* Don't detach command from terminal session or process
+	                  group. */
 
 	/* Second set of mutually exclusive flags. */
 	MF_SECOND_SET_ = 0x10,
diff --git a/src/menus/commands_menu.c b/src/menus/commands_menu.c
index 9535104..687c2e5 100644
--- a/src/menus/commands_menu.c
+++ b/src/menus/commands_menu.c
@@ -90,7 +90,7 @@ static int
 execute_commands_cb(view_t *view, menu_data_t *m)
 {
 	break_at(m->items[m->pos], ' ');
-	exec_command(m->items[m->pos], view, CIT_COMMAND);
+	cmds_dispatch1(m->items[m->pos], view, CIT_COMMAND);
 	return 0;
 }
 
diff --git a/src/menus/find_menu.c b/src/menus/find_menu.c
index 61ae636..f9cb332 100644
--- a/src/menus/find_menu.c
+++ b/src/menus/find_menu.c
@@ -27,8 +27,8 @@
 #include "../ui/statusbar.h"
 #include "../ui/ui.h"
 #include "../utils/macros.h"
-#include "../utils/path.h"
 #include "../utils/str.h"
+#include "../utils/utils.h"
 #include "../macros.h"
 #include "menus.h"
 
@@ -90,7 +90,7 @@ show_find_menu(view_t *view, int with_path, const char args[])
 		}
 		else
 		{
-			escaped_args = shell_like_escape(args, 0);
+			escaped_args = shell_arg_escape(args, curr_stats.shell_type);
 			macros[M_p].value = escaped_args;
 
 			custom_args = format_str("%s %s", DEFAULT_PREDICATE, escaped_args);
diff --git a/src/menus/grep_menu.c b/src/menus/grep_menu.c
index dab8ce6..8dd6be5 100644
--- a/src/menus/grep_menu.c
+++ b/src/menus/grep_menu.c
@@ -27,8 +27,8 @@
 #include "../ui/statusbar.h"
 #include "../ui/ui.h"
 #include "../utils/macros.h"
-#include "../utils/path.h"
 #include "../utils/str.h"
+#include "../utils/utils.h"
 #include "../macros.h"
 #include "menus.h"
 
@@ -76,7 +76,7 @@ show_grep_menu(view_t *view, const char args[], int invert)
 	macros[M_A].value = args;
 	if(args[0] != '-')
 	{
-		escaped_args = shell_like_escape(args, 0);
+		escaped_args = shell_arg_escape(args, curr_stats.shell_type);
 		macros[M_a].value = escaped_args;
 	}
 
diff --git a/src/menus/history_menu.c b/src/menus/history_menu.c
index e805ca5..abaeeb5 100644
--- a/src/menus/history_menu.c
+++ b/src/menus/history_menu.c
@@ -24,6 +24,7 @@
 #include "../cfg/config.h"
 #include "../modes/cmdline.h"
 #include "../modes/menu.h"
+#include "../modes/normal.h"
 #include "../ui/ui.h"
 #include "../utils/hist.h"
 #include "../utils/string_array.h"
@@ -121,19 +122,21 @@ execute_history_cb(view_t *view, menu_data_t *m)
 	{
 		case CMDHISTORY:
 			hists_commands_save(line);
-			exec_commands(line, view, CIT_COMMAND);
+			cmds_dispatch(line, view, CIT_COMMAND);
 			break;
 		case FSEARCHHISTORY:
 			hists_search_save(line);
-			exec_command(line, view, CIT_FSEARCH_PATTERN);
+			modnorm_set_search_attrs(/*count=*/1, /*last_search_backward=*/0);
+			cmds_dispatch1(line, view, CIT_FSEARCH_PATTERN);
 			break;
 		case BSEARCHHISTORY:
 			hists_search_save(line);
-			exec_command(line, view, CIT_BSEARCH_PATTERN);
+			modnorm_set_search_attrs(/*count=*/1, /*last_search_backward=*/1);
+			cmds_dispatch1(line, view, CIT_BSEARCH_PATTERN);
 			break;
 		case FILTERHISTORY:
 			hists_filter_save(line);
-			exec_command(line, view, CIT_FILTER_PATTERN);
+			cmds_dispatch1(line, view, CIT_FILTER_PATTERN);
 			break;
 		case EXPRREGHISTORY:
 		case PROMPTHISTORY:
@@ -155,10 +158,20 @@ history_khandler(view_t *view, menu_data_t *m, const wchar_t keys[])
 		CmdLineSubmode submode = CLS_COMMAND;
 		switch((HistoryType)m->extra_data)
 		{
-			case CMDHISTORY:     submode = CLS_COMMAND; break;
-			case FSEARCHHISTORY: submode = CLS_FSEARCH; break;
-			case BSEARCHHISTORY: submode = CLS_BSEARCH; break;
-			case FILTERHISTORY:  submode = CLS_FILTER; break;
+			case CMDHISTORY:
+				submode = CLS_COMMAND;
+				break;
+			case FSEARCHHISTORY:
+				submode = CLS_FSEARCH;
+				modnorm_set_search_attrs(/*count=*/1, /*last_search_backward=*/0);
+				break;
+			case BSEARCHHISTORY:
+				submode = CLS_BSEARCH;
+				modnorm_set_search_attrs(/*count=*/1, /*last_search_backward=*/1);
+				break;
+			case FILTERHISTORY:
+				submode = CLS_FILTER;
+				break;
 
 			case EXPRREGHISTORY:
 			case PROMPTHISTORY:
diff --git a/src/menus/jobs_menu.c b/src/menus/jobs_menu.c
index 278deb9..68c81bf 100644
--- a/src/menus/jobs_menu.c
+++ b/src/menus/jobs_menu.c
@@ -36,6 +36,8 @@ static int execute_jobs_cb(view_t *view, menu_data_t *m);
 static KHandlerResponse jobs_khandler(view_t *view, menu_data_t *m,
 		const wchar_t keys[]);
 static int cancel_job(menu_data_t *m, bg_job_t *job);
+static void reload_jobs_list(menu_data_t *m);
+static char * format_job_item(bg_job_t *job);
 static void show_job_errors(view_t *view, menu_data_t *m, bg_job_t *job);
 static KHandlerResponse errs_khandler(view_t *view, menu_data_t *m,
 		const wchar_t keys[]);
@@ -46,53 +48,12 @@ static menu_data_t jobs_m;
 int
 show_jobs_menu(view_t *view)
 {
-	bg_job_t *p;
-	int i;
-
 	menus_init_data(&jobs_m, view, strdup("Pid --- Command"),
 			strdup("No jobs currently running"));
 	jobs_m.execute_handler = &execute_jobs_cb;
 	jobs_m.key_handler = &jobs_khandler;
 
-	bg_check();
-
-	p = bg_jobs;
-
-	i = 0;
-	while(p != NULL)
-	{
-		if(bg_job_is_running(p))
-		{
-			char info_buf[24];
-			char item_buf[sizeof(info_buf) + strlen(p->cmd) + 1024];
-
-			if(p->type == BJT_COMMAND)
-			{
-				snprintf(info_buf, sizeof(info_buf), "%" PRINTF_ULL,
-						(unsigned long long)p->pid);
-			}
-			else if(p->bg_op.total == BG_UNDEFINED_TOTAL)
-			{
-				snprintf(info_buf, sizeof(info_buf), "n/a");
-			}
-			else
-			{
-				snprintf(info_buf, sizeof(info_buf), "%d/%d", p->bg_op.done + 1,
-						p->bg_op.total);
-			}
-
-			snprintf(item_buf, sizeof(item_buf), "%-8s  %s%s", info_buf, p->cmd,
-					bg_job_cancelled(p) ? " (cancelling...)" : "");
-			i = add_to_string_array(&jobs_m.items, i, item_buf);
-			jobs_m.void_data = reallocarray(jobs_m.void_data, i,
-					sizeof(*jobs_m.void_data));
-			jobs_m.void_data[i - 1] = p;
-		}
-
-		p = p->next;
-	}
-
-	jobs_m.len = i;
+	reload_jobs_list(&jobs_m);
 
 	return menus_enter(jobs_m.state, view);
 }
@@ -127,6 +88,13 @@ jobs_khandler(view_t *view, menu_data_t *m, const wchar_t keys[])
 		show_job_errors(view, m, m->void_data[m->pos]);
 		return KHR_REFRESH_WINDOW;
 	}
+	else if(wcscmp(keys, L"r") == 0)
+	{
+		reload_jobs_list(m);
+		menus_set_pos(m->state, m->pos);
+		menus_partial_redraw(m->state);
+		return KHR_REFRESH_WINDOW;
+	}
 	/* TODO: maybe use DD for forced termination? */
 	return KHR_UNHANDLED;
 }
@@ -146,9 +114,7 @@ cancel_job(menu_data_t *m, bg_job_t *job)
 		{
 			if(bg_job_cancel(job))
 			{
-				char *new_line = format_str("%s (cancelling...)", m->items[m->pos]);
-				free(m->items[m->pos]);
-				m->items[m->pos] = new_line;
+				put_string(&m->items[m->pos], format_job_item(job));
 			}
 			break;
 		}
@@ -157,6 +123,82 @@ cancel_job(menu_data_t *m, bg_job_t *job)
 	return (p != NULL);
 }
 
+/* (Re)loads list of jobs into the menu. */
+static void
+reload_jobs_list(menu_data_t *m)
+{
+	free(m->void_data);
+	free_string_array(m->items, m->len);
+
+	m->void_data = NULL;
+	m->items = NULL;
+	m->len = 0;
+
+	bg_check();
+
+	int len = 0;
+	bg_job_t *p;
+	for(p = bg_jobs; p != NULL; p = p->next)
+	{
+		if(!bg_job_is_running(p))
+		{
+			continue;
+		}
+
+		char *item = format_job_item(p);
+		if(item == NULL)
+		{
+			continue;
+		}
+
+		int new_i = put_into_string_array(&m->items, len, item);
+		if(new_i != len + 1)
+		{
+			free(item);
+			continue;
+		}
+
+		void **new_data = reallocarray(m->void_data, new_i, sizeof(*m->void_data));
+		if(new_data == NULL)
+		{
+			free(item);
+			continue;
+		}
+
+		m->void_data = new_data;
+		m->void_data[len] = p;
+
+		++len;
+	}
+
+	m->len = len;
+}
+
+/* Formats single menu line that describes state of the job.  Returns formatted
+ * string or NULL on error. */
+static char *
+format_job_item(bg_job_t *job)
+{
+	char info_buf[24];
+	if(job->type == BJT_COMMAND)
+	{
+		snprintf(info_buf, sizeof(info_buf), "%" PRINTF_ULL,
+				(unsigned long long)job->pid);
+	}
+	else if(job->bg_op.total == BG_UNDEFINED_TOTAL)
+	{
+		snprintf(info_buf, sizeof(info_buf), "n/a");
+	}
+	else
+	{
+		snprintf(info_buf, sizeof(info_buf), "%d/%d", job->bg_op.done + 1,
+				job->bg_op.total);
+	}
+
+	const char *cancelled = (bg_job_cancelled(job) ? "(cancelling...) " : "");
+	return format_str("%-8s  %s%s", info_buf, cancelled, job->cmd);
+}
+
 /* Shows job errors if there is something and the job is still running.
  * Switches to separate menu description. */
 static void
diff --git a/src/menus/locate_menu.c b/src/menus/locate_menu.c
index 0ccc9d3..a83ede0 100644
--- a/src/menus/locate_menu.c
+++ b/src/menus/locate_menu.c
@@ -26,8 +26,8 @@
 #include "../ui/statusbar.h"
 #include "../ui/ui.h"
 #include "../utils/macros.h"
-#include "../utils/path.h"
 #include "../utils/str.h"
+#include "../utils/utils.h"
 #include "../macros.h"
 #include "../running.h"
 #include "menus.h"
@@ -42,24 +42,27 @@ show_locate_menu(view_t *view, const char args[])
 	char *cmd;
 	char *margs;
 	int save_msg;
-	custom_macro_t macros[] = {
-		[M_a] = { .letter = 'a', .value = args, .uses_left = 1, .group = -1 },
 
-		[M_u] = { .letter = 'u', .value = "",   .uses_left = 1, .group = -1 },
-		[M_U] = { .letter = 'U', .value = "",   .uses_left = 1, .group = -1 },
-	};
+	margs = (args[0] == '-') ? strdup(args)
+	                         : shell_arg_escape(args, curr_stats.shell_type);
 
 	static menu_data_t m;
-	margs = (args[0] == '-') ? strdup(args) : shell_like_escape(args, 0);
 	menus_init_data(&m, view, format_str("Locate %s", margs),
 			strdup("No files found"));
-	free(margs);
+
+	custom_macro_t macros[] = {
+		[M_a] = { .letter = 'a', .value = margs, .uses_left = 1, .group = -1 },
+
+		[M_u] = { .letter = 'u', .value = "",    .uses_left = 1, .group = -1 },
+		[M_U] = { .letter = 'U', .value = "",    .uses_left = 1, .group = -1 },
+	};
 
 	m.stashable = 1;
 	m.execute_handler = &execute_locate_cb;
 	m.key_handler = &menus_def_khandler;
 
 	cmd = ma_expand_custom(cfg.locate_prg, ARRAY_LEN(macros), macros, MA_NOOPT);
+	free(margs);
 
 	MacroFlags flags = MF_NONE;
 	if(macros[M_u].explicit_use)
diff --git a/src/menus/map_menu.c b/src/menus/map_menu.c
index 02c27a0..b3e7e9d 100644
--- a/src/menus/map_menu.c
+++ b/src/menus/map_menu.c
@@ -26,6 +26,7 @@
 #include <string.h> /* strdup() strlen() strcat() */
 #include <wchar.h> /* wcsncmp() wcslen() */
 
+#include "../compat/curses.h"
 #include "../compat/reallocarray.h"
 #include "../engine/keys.h"
 #include "../modes/modes.h"
@@ -88,6 +89,12 @@ add_mapping_item(const wchar_t lhs[], const wchar_t rhs[], const char descr[])
 {
 	enum { MAP_WIDTH = 11 };
 
+	/* Skip group of mouse events, can't print them nicely in this form. */
+	if(lhs[0] == K(KEY_MOUSE))
+	{
+		return;
+	}
+
 	const int is_separator = (lhs[0] == L'\0');
 	if(!is_separator)
 	{
diff --git a/src/menus/media_menu.c b/src/menus/media_menu.c
index c3c174a..143984f 100644
--- a/src/menus/media_menu.c
+++ b/src/menus/media_menu.c
@@ -138,6 +138,8 @@ media_khandler(struct view_t *view, menu_data_t *m, const wchar_t keys[])
 	if(wcscmp(keys, L"r") == 0)
 	{
 		reload_list(m);
+		menus_set_pos(m->state, m->pos);
+		menus_partial_redraw(m->state);
 		return KHR_REFRESH_WINDOW;
 	}
 	else if(wcscmp(keys, L"[") == 0)
@@ -413,7 +415,7 @@ mediaprg_mount(const char data[], menu_data_t *m)
 	const char *action = (mount ? "mount" : "unmount");
 	const char *description = (mount ? "Mounting" : "Unmounting");
 
-	char *escaped_path = shell_like_escape(path, 0);
+	char *escaped_path = shell_arg_escape(path, curr_stats.shell_type);
 	char *cmd = format_str("%s %s %s", cfg.media_prg, action, escaped_path);
 	if(rn_shell(cmd, PAUSE_NEVER, 0, SHELL_BY_APP) == 0)
 	{
diff --git a/src/menus/menus.c b/src/menus/menus.c
index b3d5949..d6b268a 100644
--- a/src/menus/menus.c
+++ b/src/menus/menus.c
@@ -310,7 +310,7 @@ menus_set_pos(menu_state_t *ms, int pos)
 static void
 show_position_in_menu(const menu_data_t *m)
 {
-	if(vle_mode_is(CMDLINE_MODE))
+	if(modes_is_cmdline_like())
 	{
 		return;
 	}
@@ -864,7 +864,7 @@ menus_capture(view_t *view, const char cmd[], int user_sh, menu_data_t *m,
 	if(ma_flags_present(flags, MF_CUSTOMVIEW_OUTPUT) ||
 			ma_flags_present(flags, MF_VERYCUSTOMVIEW_OUTPUT))
 	{
-		rn_for_flist(view, cmd, m->title, flags);
+		rn_for_flist(view, cmd, m->title, user_sh, flags);
 		menus_reset_data(m);
 		return 0;
 	}
@@ -1177,9 +1177,9 @@ menus_search_reset_hilight(menu_state_t *m)
 }
 
 int
-menus_search_matched(menu_state_t *m)
+menus_search_matched(const menu_data_t *m)
 {
-	return m->matching_entries;
+	return m->state->matching_entries;
 }
 
 void
diff --git a/src/menus/menus.h b/src/menus/menus.h
index f9d1644..ed3db53 100644
--- a/src/menus/menus.h
+++ b/src/menus/menus.h
@@ -170,7 +170,7 @@ void menus_search_repeat(menu_state_t *m, int backward);
 void menus_search_print_msg(const menu_data_t *m);
 
 /* Retrieves number of search matches in the menu.  Returns the number. */
-int menus_search_matched(menu_state_t *m);
+int menus_search_matched(const menu_data_t *m);
 
 /* Auxiliary functions related to menus. */
 
diff --git a/src/menus/plugins_menu.c b/src/menus/plugins_menu.c
index 0cb2d1f..d5dd2bd 100644
--- a/src/menus/plugins_menu.c
+++ b/src/menus/plugins_menu.c
@@ -107,13 +107,14 @@ show_plugin_log(view_t *view, menu_data_t *m, plug_t *plug)
 	}
 	else
 	{
-		static menu_data_t m;
+		static menu_data_t log_m;
 
-		menus_init_data(&m, view, format_str("Plugin log (%s)", plug->name), NULL);
-		m.key_handler = &log_khandler;
-		m.items = break_into_lines(plug->log, plug->log_len, &m.len, 0);
+		menus_init_data(&log_m, view, format_str("Plugin log (%s)", plug->name),
+				NULL);
+		log_m.key_handler = &log_khandler;
+		log_m.items = break_into_lines(plug->log, plug->log_len, &log_m.len, 0);
 
-		modmenu_reenter(&m);
+		modmenu_reenter(&log_m);
 	}
 }
 
diff --git a/src/modes/cmdline.c b/src/modes/cmdline.c
index 3821672..8db88a5 100644
--- a/src/modes/cmdline.c
+++ b/src/modes/cmdline.c
@@ -17,6 +17,7 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
  */
 
+#define CMDLINE_IMPL
 #include "cmdline.h"
 
 #include <curses.h>
@@ -64,6 +65,7 @@
 #include "../flist_pos.h"
 #include "../flist_sel.h"
 #include "../marks.h"
+#include "../running.h"
 #include "../search.h"
 #include "../status.h"
 #include "dialogs/attr_dialog.h"
@@ -74,76 +76,8 @@
 #include "visual.h"
 #include "wk.h"
 
-/* History search mode. */
-typedef enum
-{
-	HIST_NONE,   /* No search in history is active. */
-	HIST_GO,     /* Retrieving items from history one by one. */
-	HIST_SEARCH  /* Retrieving items that match entered prefix skipping others. */
-}
-HIST;
-
-/* Describes possible states of a prompt used for interactive search. */
-typedef enum
-{
-	PS_NORMAL,        /* Normal state (empty input or input is OK). */
-	PS_WRONG_PATTERN, /* Pattern contains a mistake. */
-	PS_NO_MATCH,      /* Pattern is OK, but no matches found. */
-}
-PromptState;
-
-/* Holds state of the command-line editing mode. */
-typedef struct
-{
-	/* Mode management. */
-
-	/* Mode that entered command-line mode. */
-	int prev_mode;
-	/* Kind of command-line mode. */
-	CmdLineSubmode sub_mode;
-	/* Whether current submode allows external editing. */
-	int sub_mode_allows_ee;
-	/* Extra parameter for submode-related calls. */
-	void *sub_mode_ptr;
-
-	/* Line editing state. */
-	wchar_t *line;                /* The line reading. */
-	wchar_t *initial_line;        /* Initial state of the line. */
-	int index;                    /* Index of the current character in cmdline. */
-	int curs_pos;                 /* Position of the cursor in status bar. */
-	int len;                      /* Length of the string. */
-	int cmd_pos;                  /* Position in the history. */
-	wchar_t prompt[NAME_MAX + 1]; /* Prompt message. */
-	int prompt_wid;               /* Width of the prompt. */
-
-	/* Dot completion. */
-	int dot_pos;      /* History position or < 0 if it's not active. */
-	size_t dot_index; /* Line index. */
-	size_t dot_len;   /* Previous completion length. */
-
-	/* Command completion. */
-	size_t prefix_len;          /* Prefix length for the active completion. */
-	int complete_continue;      /* If non-zero, continue previous completion. */
-	int reverse_completion;     /* Completion in the opposite direction. */
-	complete_cmd_func complete; /* Completion function. */
-
-	/* History completion. */
-	HIST history_search; /* One of the HIST_* constants. */
-	int hist_search_len; /* Length of history search pattern. */
-	wchar_t *line_buf;   /* Content of line before using history. */
-
-	/* For search prompt. */
-	int search_mode; /* If it's a search prompt. */
-	int old_top;     /* Saved top for interactive searching. */
-	int old_pos;     /* Saved position for interactive searching. */
-
-	/* Other state. */
-	int line_edited;         /* Cache for whether input line changed flag. */
-	int enter_mapping_state; /* The mapping state at entering the mode. */
-	int expanding_abbrev;    /* Abbreviation expansion is in progress. */
-	PromptState state;       /* Prompt state with regard to current input. */
-}
-line_stats_t;
+/* Prompt prefix when navigation is enabled. */
+#define NAV_PREFIX L"(nav)"
 
 /* Stashed store of the state to support limited recursion. */
 static line_stats_t prev_input_stat;
@@ -162,18 +96,27 @@ static void handle_nonempty_input(void);
 static void update_state(int result, int nmatches);
 static void set_local_filter(const char value[]);
 static wchar_t * wcsins(wchar_t src[], const wchar_t ins[], int pos);
+static int enter_submode(CmdLineSubmode sub_mode, const char initial[],
+		int reenter);
 static void prepare_cmdline_mode(const wchar_t prompt[], const wchar_t cmd[],
-		complete_cmd_func complete, CmdLineSubmode sub_mode, int allow_ee,
-		void *sub_mode_ptr);
-static void save_view_port(void);
+		complete_cmd_func complete, CmdLineSubmode sub_mode, int allow_ee);
+static void init_line_stats(line_stats_t *stat, const wchar_t prompt[],
+		const wchar_t initial[], complete_cmd_func complete,
+		CmdLineSubmode sub_mode, int allow_ee, int prev_mode);
+static void save_view_port(line_stats_t *stat, view_t *view);
 static void set_view_port(void);
 static int is_line_edited(void);
-static void leave_cmdline_mode(void);
+static void leave_cmdline_mode(int cancelled);
+static void free_line_stats(line_stats_t *stat);
+static void cmd_ctrl_a(key_info_t key_info, keys_info_t *keys_info);
+static void cmd_ctrl_b(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_ctrl_c(key_info_t key_info, keys_info_t *keys_info);
+static void cmd_ctrl_e(key_info_t key_info, keys_info_t *keys_info);
+static void cmd_ctrl_f(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_ctrl_g(key_info_t key_info, keys_info_t *keys_info);
 static CmdInputType cls_to_editable_cit(CmdLineSubmode sub_mode);
 static void extedit_prompt(const char input[], int cursor_col, int is_expr_reg,
-		prompt_cb cb);
+		prompt_cb cb, void *cb_arg);
 static void cmd_ctrl_rb(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_ctrl_h(key_info_t key_info, keys_info_t *keys_info);
 static int should_quit_on_backspace(void);
@@ -185,8 +128,10 @@ static void draw_wild_menu(int op);
 static int draw_wild_bar(int *last_pos, int *pos, int *len);
 static int draw_wild_popup(int *last_pos, int *pos, int *len);
 static int compute_wild_menu_height(void);
+static void cmd_ctrl_j(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_ctrl_k(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_return(key_info_t key_info, keys_info_t *keys_info);
+static void nav_open(void);
 static int is_input_line_empty(void);
 static void expand_abbrev(void);
 TSTATIC const wchar_t * extract_abbrev(line_stats_t *stat, int *pos,
@@ -195,18 +140,16 @@ static void exec_abbrev(const wchar_t abbrev_rhs[], int no_remap, int pos);
 static void save_input_to_history(const keys_info_t *keys_info,
 		const char input[]);
 static void save_prompt_to_history(const char input[], int is_expr_reg);
-static void finish_prompt_submode(const char input[], prompt_cb cb);
+static void finish_prompt_submode(const char input[], prompt_cb cb,
+		void *cb_arg);
 static CmdInputType search_cls_to_cit(CmdLineSubmode sub_mode);
 static int is_forward_search(CmdLineSubmode sub_mode);
 static int is_backward_search(CmdLineSubmode sub_mode);
 static int replace_wstring(wchar_t **str, const wchar_t with[]);
 static void cmd_ctrl_n(key_info_t key_info, keys_info_t *keys_info);
-#ifdef ENABLE_EXTENDED_KEYS
-static void cmd_down(key_info_t key_info, keys_info_t *keys_info);
-#endif /* ENABLE_EXTENDED_KEYS */
 static void hist_next(line_stats_t *stat, const hist_t *hist, size_t len);
 static void cmd_ctrl_requals(key_info_t key_info, keys_info_t *keys_info);
-static void expr_reg_prompt_cb(const char expr[]);
+static void expr_reg_prompt_cb(const char expr[], void *arg);
 static int expr_reg_prompt_completion(const char cmd[], void *arg);
 static void cmd_ctrl_u(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_ctrl_w(key_info_t key_info, keys_info_t *keys_info);
@@ -240,21 +183,30 @@ static wchar_t * next_dot_completion(void);
 static int insert_dot_completion(const wchar_t completion[]);
 static int insert_str(const wchar_t str[]);
 static void find_next_word(void);
-static void cmd_left(key_info_t key_info, keys_info_t *keys_info);
-static void cmd_right(key_info_t key_info, keys_info_t *keys_info);
-static void cmd_home(key_info_t key_info, keys_info_t *keys_info);
-static void cmd_end(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_delete(key_info_t key_info, keys_info_t *keys_info);
 static void update_cursor(void);
 TSTATIC void hist_prev(line_stats_t *stat, const hist_t *hist, size_t len);
 static int replace_input_line(line_stats_t *stat, const char new[]);
 static const hist_t * pick_hist(void);
+static int is_cmdmode(int mode);
 static void update_cmdline(line_stats_t *stat);
 static int get_required_height(void);
+static void cmd_ctrl_o(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_ctrl_p(key_info_t key_info, keys_info_t *keys_info);
+static void move_view_cursor(line_stats_t *stat, view_t *view, int new_pos);
 static void cmd_ctrl_t(key_info_t key_info, keys_info_t *keys_info);
+static void cmd_ctrl_y(key_info_t key_info, keys_info_t *keys_info);
+static void nav_start(line_stats_t *stat);
+static void nav_stop(line_stats_t *stat);
 #ifdef ENABLE_EXTENDED_KEYS
+static void cmd_down(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_up(key_info_t key_info, keys_info_t *keys_info);
+static void cmd_left(key_info_t key_info, keys_info_t *keys_info);
+static void cmd_right(key_info_t key_info, keys_info_t *keys_info);
+static void cmd_home(key_info_t key_info, keys_info_t *keys_info);
+static void cmd_end(key_info_t key_info, keys_info_t *keys_info);
+static void cmd_page_up(key_info_t key_info, keys_info_t *keys_info);
+static void cmd_page_down(key_info_t key_info, keys_info_t *keys_info);
 #endif /* ENABLE_EXTENDED_KEYS */
 static void update_cmdline_size(void);
 TSTATIC int line_completion(line_stats_t *stat);
@@ -268,15 +220,19 @@ static wchar_t * wcsdel(wchar_t *src, int pos, int len);
 static void stop_completion(void);
 static void stop_dot_completion(void);
 static void stop_regular_completion(void);
+TSTATIC line_stats_t * get_line_stats(void);
+static void handle_mouse_event(key_info_t key_info, keys_info_t *keys_info);
 
 static keys_add_info_t builtin_cmds[] = {
 	{WK_C_c,             {{&cmd_ctrl_c}, .descr = "leave cmdline mode"}},
 	{WK_C_g,             {{&cmd_ctrl_g}, .descr = "edit cmdline in editor"}},
 	{WK_C_h,             {{&cmd_ctrl_h}, .descr = "remove char to the left"}},
 	{WK_C_i,             {{&cmd_ctrl_i}, .descr = "start/continue completion"}},
+	{WK_C_j,             {{&cmd_ctrl_j}, .descr = "nav: leave without moving the cursor"}},
 	{WK_C_k,             {{&cmd_ctrl_k}, .descr = "remove line part to the right"}},
 	{WK_CR,              {{&cmd_return}, .descr = "execute/accept input"}},
 	{WK_C_n,             {{&cmd_ctrl_n}, .descr = "recall next history item"}},
+	{WK_C_o,             {{&cmd_ctrl_o}, .descr = "nav: go to parent directory"}},
 	{WK_C_p,             {{&cmd_ctrl_p}, .descr = "recall previous history item"}},
 	{WK_C_t,             {{&cmd_ctrl_t}, .descr = "swap adjacent characters"}},
 	{WK_ESC,             {{&cmd_ctrl_c}, .descr = "leave cmdline mode"}},
@@ -285,14 +241,15 @@ static keys_add_info_t builtin_cmds[] = {
 	{WK_C_USCORE,        {{&cmd_ctrl_underscore}, .descr = "reset completion"}},
 	{WK_DELETE,          {{&cmd_ctrl_h}, .descr = "remove char to the left"}},
 	{WK_ESC L"[Z",       {{&cmd_shift_tab}, .descr = "complete in reverse order"}},
-	{WK_C_b,             {{&cmd_left},   .descr = "move cursor to the left"}},
-	{WK_C_f,             {{&cmd_right},  .descr = "move cursor to the right"}},
-	{WK_C_a,             {{&cmd_home},   .descr = "move cursor to the beginning"}},
-	{WK_C_e,             {{&cmd_end},    .descr = "move cursor to the end"}},
+	{WK_C_b,             {{&cmd_ctrl_b}, .descr = "move cursor to the left"}},
+	{WK_C_f,             {{&cmd_ctrl_f}, .descr = "move cursor to the right"}},
+	{WK_C_a,             {{&cmd_ctrl_a}, .descr = "move cursor to the beginning"}},
+	{WK_C_e,             {{&cmd_ctrl_e}, .descr = "move cursor to the end"}},
 	{WK_C_d,             {{&cmd_delete}, .descr = "delete current character"}},
 	{WK_C_r WK_EQUALS,   {{&cmd_ctrl_requals}, .descr = "invoke expression register prompt"}},
 	{WK_C_u,             {{&cmd_ctrl_u}, .descr = "remove line part to the left"}},
 	{WK_C_w,             {{&cmd_ctrl_w}, .descr = "remove word to the left"}},
+	{WK_C_y,             {{&cmd_ctrl_y}, .descr = "toggle navigation"}},
 	{WK_C_x WK_SLASH,    {{&cmd_ctrl_xslash}, .descr = "insert last search pattern"}},
 	{WK_C_x WK_a,        {{&cmd_ctrl_xa}, .descr = "insert implicit permanent filter value"}},
 	{WK_C_x WK_c,        {{&cmd_ctrl_xc}, .descr = "insert current file name"}},
@@ -320,15 +277,18 @@ static keys_add_info_t builtin_cmds[] = {
 #endif
 #ifdef ENABLE_EXTENDED_KEYS
 	{{K(KEY_BACKSPACE)}, {{&cmd_ctrl_h}, .descr = "remove char to the left"}},
-	{{K(KEY_DOWN)},      {{&cmd_down},   .descr = "prefix-complete next history item"}},
-	{{K(KEY_UP)},        {{&cmd_up},     .descr = "prefix-complete previous history item"}},
-	{{K(KEY_LEFT)},      {{&cmd_left},   .descr = "move cursor to the left"}},
-	{{K(KEY_RIGHT)},     {{&cmd_right},  .descr = "move cursor to the right"}},
-	{{K(KEY_HOME)},      {{&cmd_home},   .descr = "move cursor to the beginning"}},
-	{{K(KEY_END)},       {{&cmd_end},    .descr = "move cursor to the end"}},
+	{{K(KEY_DOWN)},      {{&cmd_down},   .descr = "prefix-complete next history item/next item"}},
+	{{K(KEY_UP)},        {{&cmd_up},     .descr = "prefix-complete previous history item/prev item"}},
+	{{K(KEY_LEFT)},      {{&cmd_left},   .descr = "move cursor to the left/parent dir"}},
+	{{K(KEY_RIGHT)},     {{&cmd_right},  .descr = "move cursor to the right/enter entry"}},
+	{{K(KEY_HOME)},      {{&cmd_home},   .descr = "move cursor to the beginning/first item"}},
+	{{K(KEY_END)},       {{&cmd_end},    .descr = "move cursor to the end/last item"}},
 	{{K(KEY_DC)},        {{&cmd_delete}, .descr = "delete current character"}},
 	{{K(KEY_BTAB)},      {{&cmd_shift_tab}, .descr = "complete in reverse order"}},
+	{{K(KEY_PPAGE)},     {{&cmd_page_up},   .descr = "nav: scroll page up"}},
+	{{K(KEY_NPAGE)},     {{&cmd_page_down}, .descr = "nav: scroll page down"}},
 #endif /* ENABLE_EXTENDED_KEYS */
+	{{K(KEY_MOUSE)},     {{&handle_mouse_event}, FOLLOWED_BY_NONE}},
 };
 
 void
@@ -344,6 +304,16 @@ modcline_init(void)
 	(void)ret_code;
 }
 
+void
+modnav_init(void)
+{
+	vle_keys_set_def_handler(NAV_MODE, &def_handler);
+
+	int ret_code = vle_keys_add(builtin_cmds, ARRAY_LEN(builtin_cmds), NAV_MODE);
+	assert(ret_code == 0);
+	(void)ret_code;
+}
+
 /* Handles all keys uncaught by shortcuts.  Returns zero on success and non-zero
  * on error. */
 static int
@@ -383,7 +353,7 @@ def_handler(wchar_t key)
 	p = reallocarray(input_stat.line, input_stat.len + 2, sizeof(wchar_t));
 	if(p == NULL)
 	{
-		leave_cmdline_mode();
+		leave_cmdline_mode(/*cancelled=*/1);
 		return 0;
 	}
 
@@ -460,8 +430,6 @@ draw_cmdline_text(line_stats_t *stat)
 static void
 input_line_changed(void)
 {
-	static wchar_t *previous;
-
 	if(!cfg.inc_search ||
 			(!input_stat.search_mode && input_stat.sub_mode != CLS_FILTER))
 	{
@@ -470,22 +438,23 @@ input_line_changed(void)
 
 	/* Hide cursor during view update, otherwise user might notice it blinking in
 	 * wrong places. */
-	curs_set(0);
+	ui_set_cursor(/*visibility=*/0);
 
 	/* set_view_port() should not be called if none of the conditions are true. */
 
 	input_stat.state = PS_NORMAL;
 	if(is_input_line_empty())
 	{
-		free(previous);
-		previous = NULL;
+		free(input_stat.last_line);
+		input_stat.last_line = NULL;
 
 		set_view_port();
 		handle_empty_input();
 	}
-	else if(previous == NULL || wcscmp(previous, input_stat.line) != 0)
+	else if(input_stat.last_line == NULL ||
+			wcscmp(input_stat.last_line, input_stat.line) != 0)
 	{
-		(void)replace_wstring(&previous, input_stat.line);
+		(void)replace_wstring(&input_stat.last_line, input_stat.line);
 
 		set_view_port();
 		handle_nonempty_input();
@@ -505,7 +474,7 @@ input_line_changed(void)
 	/* Hardware cursor is moved on the screen only on refresh, so refresh status
 	 * bar to force cursor moving there before it becomes visible again. */
 	ui_refresh_win(status_bar);
-	curs_set(1);
+	ui_set_cursor(/*visibility=*/1);
 }
 
 /* Provides reaction for empty input during interactive search/filtering. */
@@ -515,9 +484,9 @@ handle_empty_input(void)
 	/* Clear selection/highlight. */
 	if(input_stat.prev_mode == MENU_MODE)
 	{
-		(void)menus_search("", input_stat.sub_mode_ptr, 0);
+		(void)menus_search("", input_stat.menu, 0);
 	}
-	else if(cfg.hl_search)
+	else if(cfg.hl_search && input_stat.prev_mode != VISUAL_MODE)
 	{
 		flist_sel_stash(curr_view);
 	}
@@ -527,6 +496,11 @@ handle_empty_input(void)
 		ui_view_reset_search_highlight(curr_view);
 	}
 
+	if(input_stat.prev_mode == VISUAL_MODE)
+	{
+		modvis_update();
+	}
+
 	if(input_stat.sub_mode == CLS_FILTER)
 	{
 		set_local_filter("");
@@ -546,18 +520,20 @@ handle_nonempty_input(void)
 
 		case CLS_BSEARCH: backward = 1; /* Fall through. */
 		case CLS_FSEARCH:
-			result = modnorm_find(curr_view, mbinput, backward, 0);
+			result = modnorm_find(curr_view, mbinput, backward, /*print_msg=*/0,
+					&input_stat.search_match_found);
 			update_state(result, curr_view->matches);
 			break;
 		case CLS_VBSEARCH: backward = 1; /* Fall through. */
 		case CLS_VFSEARCH:
-			result = modvis_find(curr_view, mbinput, backward, 0);
+			result = modvis_find_interactive(curr_view, mbinput, backward,
+					&input_stat.search_match_found);
 			update_state(result, curr_view->matches);
 			break;
 		case CLS_MENU_FSEARCH:
 		case CLS_MENU_BSEARCH:
-			result = menus_search(mbinput, input_stat.sub_mode_ptr, 0);
-			update_state(result, menus_search_matched(input_stat.sub_mode_ptr));
+			result = menus_search(mbinput, input_stat.menu, /*print_erros=*/0);
+			update_state(result, menus_search_matched(input_stat.menu));
 			break;
 		case CLS_FILTER:
 			set_local_filter(mbinput);
@@ -628,23 +604,53 @@ wcsins(wchar_t src[], const wchar_t ins[], int pos)
 }
 
 void
-modcline_enter(CmdLineSubmode sub_mode, const char cmd[], void *ptr)
+modcline_enter(CmdLineSubmode sub_mode, const char initial[])
 {
-	wchar_t *wcmd;
+	assert(sub_mode != CLS_MENU_COMMAND && sub_mode != CLS_MENU_FSEARCH &&
+			sub_mode != CLS_MENU_BSEARCH &&
+			"Use modcline_in_menu() for CLS_MENU_* submodes.");
+	assert(sub_mode != CLS_PROMPT &&
+			"Use modcline_prompt() for CLS_PROMPT submode.");
+	if(enter_submode(sub_mode, initial, /*reenter=*/0) == 0 &&
+			!is_null_or_empty(initial))
+	{
+		/* Trigger handling of input if its initial value isn't empty. */
+		update_cmdline_text(&input_stat);
+	}
+}
+
+void
+modcline_in_menu(CmdLineSubmode sub_mode, struct menu_data_t *m)
+{
+	assert((sub_mode == CLS_MENU_COMMAND || sub_mode == CLS_MENU_FSEARCH ||
+			sub_mode == CLS_MENU_BSEARCH) &&
+			"modcline_in_menu() is only for CLS_MENU_* submodes.");
+
+	if(enter_submode(sub_mode, /*initial=*/"", /*reenter=*/0) == 0)
+	{
+		input_stat.menu = m;
+	}
+}
+
+/* Enters command-line editing submode.  Returns zero on success. */
+static int
+enter_submode(CmdLineSubmode sub_mode, const char initial[], int reenter)
+{
+	wchar_t *winitial;
 	const wchar_t *wprompt;
 	complete_cmd_func complete_func = NULL;
 
 	if(sub_mode == CLS_FILTER && curr_view->custom.type == CV_DIFF)
 	{
 		show_error_msg("Filtering", "No local filter for diff views");
-		return;
+		return 1;
 	}
 
-	wcmd = to_wide_force(cmd);
-	if(wcmd == NULL)
+	winitial = to_wide_force(initial);
+	if(winitial == NULL)
 	{
 		show_error_msg("Error", "Not enough memory");
-		return;
+		return 1;
 	}
 
 	if(sub_mode == CLS_COMMAND || sub_mode == CLS_MENU_COMMAND)
@@ -670,29 +676,48 @@ modcline_enter(CmdLineSubmode sub_mode, const char cmd[], void *ptr)
 		wprompt = L"E";
 	}
 
-	prepare_cmdline_mode(wprompt, wcmd, complete_func, sub_mode, /*allow_ee=*/0,
-			ptr);
-	free(wcmd);
+	if(reenter)
+	{
+		int prev_mode = input_stat.prev_mode;
+		free_line_stats(&input_stat);
+		init_line_stats(&input_stat, wprompt, winitial, complete_func, sub_mode,
+				/*allow_ee=*/0, prev_mode);
+
+		/* Just in case. */
+		curr_stats.save_msg = 1;
+
+		stats_redraw_later();
+	}
+	else
+	{
+		prepare_cmdline_mode(wprompt, winitial, complete_func, sub_mode,
+				/*allow_ee=*/0);
+	}
+
+	free(winitial);
+	return 0;
 }
 
 void
-modcline_prompt(const char prompt[], const char cmd[], prompt_cb cb,
-		complete_cmd_func complete, int allow_ee)
+modcline_prompt(const char prompt[], const char initial[], prompt_cb cb,
+		void *cb_arg, complete_cmd_func complete, int allow_ee)
 {
 	wchar_t *wprompt = to_wide_force(prompt);
-	wchar_t *wcmd = to_wide_force(cmd);
+	wchar_t *winitial = to_wide_force(initial);
 
-	if(wprompt == NULL || wcmd == NULL)
+	if(wprompt == NULL || winitial == NULL)
 	{
 		show_error_msg("Error", "Not enough memory");
 	}
 	else
 	{
-		prepare_cmdline_mode(wprompt, wcmd, complete, CLS_PROMPT, allow_ee, cb);
+		prepare_cmdline_mode(wprompt, winitial, complete, CLS_PROMPT, allow_ee);
+		input_stat.prompt_callback = cb;
+		input_stat.prompt_callback_arg = cb_arg;
 	}
 
 	free(wprompt);
-	free(wcmd);
+	free(winitial);
 }
 
 void
@@ -700,7 +725,7 @@ modcline_redraw(void)
 {
 	/* Hide cursor during redraw, otherwise user might notice it blinking in wrong
 	 * places. */
-	curs_set(0);
+	ui_set_cursor(/*visibility=*/0);
 
 	if(input_stat.prev_mode == MENU_MODE)
 	{
@@ -734,89 +759,101 @@ modcline_redraw(void)
 	}
 
 	/* Make cursor visible after all redraws. */
-	curs_set(1);
+	ui_set_cursor(/*visibility=*/1);
 }
 
 /* Performs all necessary preparations for command-line mode to start
  * operating. */
 static void
-prepare_cmdline_mode(const wchar_t prompt[], const wchar_t cmd[],
-		complete_cmd_func complete, CmdLineSubmode sub_mode, int allow_ee,
-		void *sub_mode_ptr)
+prepare_cmdline_mode(const wchar_t prompt[], const wchar_t initial[],
+		complete_cmd_func complete, CmdLineSubmode sub_mode, int allow_ee)
 {
-	if(vle_mode_get() == CMDLINE_MODE)
+	if(is_cmdmode(vle_mode_get()))
 	{
 		/* We're recursing into command-line mode. */
 		ui_sb_unlock();
 		prev_input_stat = input_stat;
 	}
 
-	input_stat.sub_mode = sub_mode;
-	input_stat.sub_mode_allows_ee = allow_ee;
-	input_stat.sub_mode_ptr = sub_mode_ptr;
-
-	input_stat.prev_mode = vle_mode_get();
-	vle_mode_set(CMDLINE_MODE, VMT_SECONDARY);
-
 	line_width = getmaxx(stdscr);
-
 	ui_sb_lock();
 
-	input_stat.line = vifm_wcsdup(cmd);
-	input_stat.initial_line = vifm_wcsdup(input_stat.line);
-	input_stat.index = wcslen(cmd);
-	input_stat.curs_pos = esc_wcswidth(input_stat.line, (size_t)-1);
-	input_stat.len = input_stat.index;
-	input_stat.cmd_pos = -1;
-	input_stat.complete_continue = 0;
-	input_stat.history_search = HIST_NONE;
-	input_stat.line_buf = NULL;
-	input_stat.reverse_completion = 0;
-	input_stat.complete = complete;
-	input_stat.search_mode = 0;
-	input_stat.dot_pos = -1;
-	input_stat.line_edited = 0;
-	input_stat.enter_mapping_state = vle_keys_mapping_state();
-	input_stat.state = PS_NORMAL;
-
-	if((is_forward_search(sub_mode) || is_backward_search(sub_mode)) &&
-			sub_mode != CLS_VWFSEARCH && sub_mode != CLS_VWBSEARCH)
-	{
-		input_stat.search_mode = 1;
-	}
-
-	if(input_stat.search_mode || sub_mode == CLS_FILTER)
-	{
-		save_view_port();
-	}
+	init_line_stats(&input_stat, prompt, initial, complete, sub_mode, allow_ee,
+			vle_mode_get());
 
-	wcsncpy(input_stat.prompt, prompt, ARRAY_LEN(input_stat.prompt));
-	input_stat.prompt_wid = esc_wcswidth(input_stat.prompt, (size_t)-1);
-	input_stat.curs_pos += input_stat.prompt_wid;
+	vle_mode_set(CMDLINE_MODE, VMT_SECONDARY);
 
 	update_cmdline_size();
-	update_cmdline_text(&input_stat);
+	draw_cmdline_text(&input_stat);
 
 	curr_stats.save_msg = 1;
 
 	if(input_stat.prev_mode == NORMAL_MODE)
-		init_commands();
+		cmds_init();
 
 	/* Make cursor visible only after all initial draws. */
-	if(curr_stats.load_stage > 0)
+	ui_set_cursor(/*visibility=*/1);
+}
+
+/* Initializes command-line status. */
+static void
+init_line_stats(line_stats_t *stat, const wchar_t prompt[],
+		const wchar_t initial[], complete_cmd_func complete,
+		CmdLineSubmode sub_mode, int allow_ee, int prev_mode)
+{
+	stat->sub_mode = sub_mode;
+	stat->navigating = 0;
+	stat->sub_mode_allows_ee = allow_ee;
+	stat->menu = NULL;
+	stat->prompt_callback = NULL;
+	stat->prompt_callback_arg = NULL;
+
+	stat->prev_mode = prev_mode;
+
+	stat->line = vifm_wcsdup(initial);
+	stat->last_line = NULL;
+	stat->initial_line = vifm_wcsdup(stat->line);
+	stat->index = wcslen(initial);
+	stat->curs_pos = esc_wcswidth(stat->line, (size_t)-1);
+	stat->len = stat->index;
+	stat->cmd_pos = -1;
+	stat->complete_continue = 0;
+	stat->history_search = HIST_NONE;
+	stat->line_buf = NULL;
+	stat->reverse_completion = 0;
+	stat->complete = complete;
+	stat->search_mode = 0;
+	stat->search_match_found = 0;
+	stat->dot_pos = -1;
+	stat->line_edited = 0;
+	stat->enter_mapping_state = vle_keys_mapping_state();
+	stat->expanding_abbrev = 0;
+	stat->state = PS_NORMAL;
+
+	if((is_forward_search(sub_mode) || is_backward_search(sub_mode)) &&
+			sub_mode != CLS_VWFSEARCH && sub_mode != CLS_VWBSEARCH)
 	{
-		curs_set(1);
+		stat->search_mode = 1;
 	}
+
+	if(stat->search_mode || sub_mode == CLS_FILTER)
+	{
+		save_view_port(stat, curr_view);
+	}
+
+	wcsncpy(stat->prompt, prompt, ARRAY_LEN(stat->prompt));
+	stat->prompt_wid = esc_wcswidth(stat->prompt, (size_t)-1);
+	stat->curs_pos += stat->prompt_wid;
 }
 
 /* Stores view port parameters (top line, current position). */
 static void
-save_view_port(void)
+save_view_port(line_stats_t *stat, view_t *view)
 {
-	if(input_stat.prev_mode != MENU_MODE)
+	if(stat->prev_mode != MENU_MODE)
 	{
-		input_stat.old_top = curr_view->top_line;
-		input_stat.old_pos = curr_view->list_pos;
+		stat->old_top = view->top_line;
+		stat->old_pos = view->list_pos;
 	}
 	else
 	{
@@ -844,11 +881,6 @@ set_view_port(void)
 		/* Filtering itself doesn't update status line. */
 		ui_stat_update(curr_view, 1);
 	}
-
-	if(input_stat.prev_mode == VISUAL_MODE)
-	{
-		modvis_update();
-	}
 }
 
 /* Checks whether line was edited since entering command-line mode. */
@@ -866,18 +898,13 @@ is_line_edited(void)
 
 /* Leaves command-line mode. */
 static void
-leave_cmdline_mode(void)
+leave_cmdline_mode(int cancelled)
 {
-	free(input_stat.line);
-	free(input_stat.initial_line);
-	free(input_stat.line_buf);
-	input_stat.line = NULL;
-	input_stat.initial_line = NULL;
-	input_stat.line_buf = NULL;
+	free_line_stats(&input_stat);
 
-	if(vle_mode_is(CMDLINE_MODE))
+	if(is_cmdmode(vle_mode_get()))
 	{
-		if(input_stat.prev_mode == CMDLINE_MODE)
+		if(is_cmdmode(input_stat.prev_mode))
 		{
 			/* We're restoring from a recursive command-line mode. */
 			input_stat = prev_input_stat;
@@ -887,14 +914,18 @@ leave_cmdline_mode(void)
 			return;
 		}
 
+		if(cancelled && input_stat.sub_mode == CLS_PROMPT)
+		{
+			/* Invoke callback with NULL for a result to inform about cancellation. */
+			finish_prompt_submode(/*input=*/NULL, input_stat.prompt_callback,
+					input_stat.prompt_callback_arg);
+		}
+
 		vle_mode_set(input_stat.prev_mode, VMT_PRIMARY);
 	}
 
 	/* Hide the cursor first. */
-	if(curr_stats.load_stage > 0)
-	{
-		curs_set(0);
-	}
+	ui_set_cursor(/*visibility=*/0);
 
 	const int multiline_status_bar = (getmaxy(status_bar) > 1);
 
@@ -925,6 +956,44 @@ leave_cmdline_mode(void)
 	}
 }
 
+/* Frees resources allocated for command-line status. */
+static void
+free_line_stats(line_stats_t *stat)
+{
+	free(input_stat.line);
+	free(input_stat.last_line);
+	free(input_stat.initial_line);
+	free(input_stat.line_buf);
+	input_stat.line = NULL;
+	input_stat.last_line = NULL;
+	input_stat.initial_line = NULL;
+	input_stat.line_buf = NULL;
+}
+
+/* Moves command-line cursor to the beginning of command-line. */
+static void
+cmd_ctrl_a(key_info_t key_info, keys_info_t *keys_info)
+{
+	input_stat.index = 0;
+	input_stat.curs_pos = input_stat.prompt_wid;
+	update_cursor();
+}
+
+/* Moves command-line cursor to the left. */
+static void
+cmd_ctrl_b(key_info_t key_info, keys_info_t *keys_info)
+{
+	input_stat.history_search = HIST_NONE;
+	stop_completion();
+
+	if(input_stat.index > 0)
+	{
+		input_stat.index--;
+		input_stat.curs_pos -= esc_wcwidth(input_stat.line[input_stat.index]);
+		update_cursor();
+	}
+}
+
 /* Initiates leaving of command-line mode and reverting related changes in other
  * parts of the interface. */
 static void
@@ -947,7 +1016,7 @@ cmd_ctrl_c(key_info_t key_info, keys_info_t *keys_info)
 	}
 
 	const line_stats_t old_input_stat = input_stat;
-	leave_cmdline_mode();
+	leave_cmdline_mode(/*cancelled=*/1);
 
 	if(old_input_stat.prev_mode == VISUAL_MODE)
 	{
@@ -960,7 +1029,7 @@ cmd_ctrl_c(key_info_t key_info, keys_info_t *keys_info)
 
 	if(old_input_stat.sub_mode == CLS_COMMAND)
 	{
-		curr_stats.save_msg = exec_commands("", curr_view, CIT_COMMAND);
+		curr_stats.save_msg = cmds_dispatch("", curr_view, CIT_COMMAND);
 	}
 	else if(old_input_stat.sub_mode == CLS_FILTER)
 	{
@@ -971,6 +1040,34 @@ cmd_ctrl_c(key_info_t key_info, keys_info_t *keys_info)
 	}
 }
 
+/* Moves command-line cursor to the end of command-line. */
+static void
+cmd_ctrl_e(key_info_t key_info, keys_info_t *keys_info)
+{
+	if(input_stat.index == input_stat.len)
+		return;
+
+	input_stat.index = input_stat.len;
+	input_stat.curs_pos = input_stat.prompt_wid +
+			esc_wcswidth(input_stat.line, (size_t)-1);
+	update_cursor();
+}
+
+/* Moves command-line cursor to the right. */
+static void
+cmd_ctrl_f(key_info_t key_info, keys_info_t *keys_info)
+{
+	input_stat.history_search = HIST_NONE;
+	stop_completion();
+
+	if(input_stat.index < input_stat.len)
+	{
+		input_stat.curs_pos += esc_wcwidth(input_stat.line[input_stat.index]);
+		input_stat.index++;
+		update_cursor();
+	}
+}
+
 /* Opens the editor with already typed in characters, gets entered line and
  * executes it as if it was typed. */
 static void
@@ -984,10 +1081,11 @@ cmd_ctrl_g(key_info_t key_info, keys_info_t *keys_info)
 		char *const mbstr = to_multibyte(input_stat.line);
 		const int prev_mode = input_stat.prev_mode;
 		const CmdLineSubmode sub_mode = input_stat.sub_mode;
-		void *const sub_mode_ptr = input_stat.sub_mode_ptr;
+		const prompt_cb prompt_callback = input_stat.prompt_callback;
+		void *const prompt_callback_arg = input_stat.prompt_callback_arg;
 		const int index = input_stat.index;
 
-		leave_cmdline_mode();
+		leave_cmdline_mode(/*cancelled=*/0);
 
 		if(sub_mode == CLS_FILTER)
 		{
@@ -996,12 +1094,13 @@ cmd_ctrl_g(key_info_t key_info, keys_info_t *keys_info)
 
 		if(prompt_ee)
 		{
-			int is_expr_reg = (prev_mode == CMDLINE_MODE);
-			extedit_prompt(mbstr, index + 1, is_expr_reg, sub_mode_ptr);
+			int is_expr_reg = is_cmdmode(prev_mode);
+			extedit_prompt(mbstr, index + 1, is_expr_reg, prompt_callback,
+					prompt_callback_arg);
 		}
 		else
 		{
-			get_and_execute_command(mbstr, index + 1, type);
+			cmds_run_ext(mbstr, index + 1, type);
 		}
 
 		free(mbstr);
@@ -1031,15 +1130,14 @@ cls_to_editable_cit(CmdLineSubmode sub_mode)
 /* Queries prompt input using external editor. */
 static void
 extedit_prompt(const char input[], int cursor_col, int is_expr_reg,
-		prompt_cb cb)
+		prompt_cb cb, void *cb_arg)
 {
 	CmdInputType type = (is_expr_reg ? CIT_EXPRREG_INPUT : CIT_PROMPT_INPUT);
-	char *const ext_cmd = get_ext_command(input, cursor_col, type);
+	char *const ext_cmd = cmds_get_ext(input, cursor_col, type);
 
 	if(ext_cmd != NULL)
 	{
 		save_prompt_to_history(ext_cmd, is_expr_reg);
-		finish_prompt_submode(ext_cmd, cb);
 	}
 	else
 	{
@@ -1049,6 +1147,8 @@ extedit_prompt(const char input[], int cursor_col, int is_expr_reg,
 		curr_stats.save_msg = 1;
 	}
 
+	/* Invoking this will NULL in case of error to report it. */
+	finish_prompt_submode(ext_cmd, cb, cb_arg);
 	free(ext_cmd);
 }
 
@@ -1367,6 +1467,20 @@ compute_wild_menu_height(void)
 	return MIN(count, MIN(10, max_height));
 }
 
+/* Leaves navigation mode without undoing cursor position or filter state. */
+static void
+cmd_ctrl_j(key_info_t key_info, keys_info_t *keys_info)
+{
+	if(input_stat.navigating)
+	{
+		if(input_stat.sub_mode == CLS_FILTER)
+		{
+			local_filter_accept(curr_view, /*update_history=*/0);
+		}
+		leave_cmdline_mode(/*cancelled=*/0);
+	}
+}
+
 static void
 cmd_ctrl_k(key_info_t key_info, keys_info_t *keys_info)
 {
@@ -1387,6 +1501,12 @@ static void
 cmd_return(key_info_t key_info, keys_info_t *keys_info)
 {
 	/* TODO: refactor this cmd_return() function. */
+	if(input_stat.navigating)
+	{
+		nav_open();
+		return;
+	}
+
 	stop_completion();
 	werase(status_bar);
 	wnoutrefresh(status_bar);
@@ -1395,7 +1515,7 @@ cmd_return(key_info_t key_info, keys_info_t *keys_info)
 
 	if(is_input_line_empty() && sub_mode == CLS_MENU_COMMAND)
 	{
-		leave_cmdline_mode();
+		leave_cmdline_mode(/*cancelled=*/0);
 		return;
 	}
 
@@ -1405,9 +1525,11 @@ cmd_return(key_info_t key_info, keys_info_t *keys_info)
 	save_input_to_history(keys_info, input);
 
 	const int prev_mode = input_stat.prev_mode;
-	void *const sub_mode_ptr = input_stat.sub_mode_ptr;
+	struct menu_data_t *const menu = input_stat.menu;
+	const prompt_cb prompt_callback = input_stat.prompt_callback;
+	void *const prompt_callback_arg = input_stat.prompt_callback_arg;
 	const int search_mode = input_stat.search_mode;
-	leave_cmdline_mode();
+	leave_cmdline_mode(/*cancelled=*/0);
 
 	if(prev_mode == VISUAL_MODE && sub_mode != CLS_VFSEARCH &&
 			sub_mode != CLS_VBSEARCH)
@@ -1429,12 +1551,12 @@ cmd_return(key_info_t key_info, keys_info_t *keys_info)
 
 		if(sub_mode == CLS_COMMAND)
 		{
-			commands_scope_start();
+			cmds_scope_start();
 		}
-		curr_stats.save_msg = exec_commands(real_start, curr_view, cmd_type);
+		curr_stats.save_msg = cmds_dispatch(real_start, curr_view, cmd_type);
 		if(sub_mode == CLS_COMMAND)
 		{
-			if(commands_scope_finish() != 0)
+			if(cmds_scope_finish() != 0)
 			{
 				curr_stats.save_msg = 1;
 			}
@@ -1442,13 +1564,13 @@ cmd_return(key_info_t key_info, keys_info_t *keys_info)
 	}
 	else if(sub_mode == CLS_PROMPT)
 	{
-		finish_prompt_submode(input, sub_mode_ptr);
+		finish_prompt_submode(input, prompt_callback, prompt_callback_arg);
 	}
 	else if(sub_mode == CLS_FILTER)
 	{
 		if(cfg.inc_search)
 		{
-			local_filter_accept(curr_view);
+			local_filter_accept(curr_view, /*update_history=*/1);
 		}
 		else
 		{
@@ -1471,13 +1593,13 @@ cmd_return(key_info_t key_info, keys_info_t *keys_info)
 			case CLS_VWBSEARCH:
 				{
 					const CmdInputType cit = search_cls_to_cit(sub_mode);
-					curr_stats.save_msg = exec_command(pattern, curr_view, cit);
+					curr_stats.save_msg = cmds_dispatch1(pattern, curr_view, cit);
 					break;
 				}
 			case CLS_MENU_FSEARCH:
 			case CLS_MENU_BSEARCH:
 				stats_refresh_later();
-				curr_stats.save_msg = menus_search(pattern, sub_mode_ptr, 1);
+				curr_stats.save_msg = menus_search(pattern, menu, 1);
 				break;
 
 			default:
@@ -1490,24 +1612,83 @@ cmd_return(key_info_t key_info, keys_info_t *keys_info)
 		if(prev_mode == MENU_MODE)
 		{
 			modmenu_partial_redraw();
-			menus_search_print_msg(sub_mode_ptr);
+			menus_search_print_msg(menu);
 			curr_stats.save_msg = 1;
 		}
 		else
 		{
-			/* In case of successful search and 'hlsearch' option set, a message like
-			* "n files selected" is printed automatically. */
-			if(curr_view->matches == 0 || !cfg.hl_search)
-			{
-				print_search_msg(curr_view, is_backward_search(sub_mode));
-				curr_stats.save_msg = 1;
-			}
+			curr_stats.save_msg = print_search_result(curr_view,
+					input_stat.search_match_found, is_backward_search(sub_mode),
+					&print_search_msg);
 		}
 	}
 
 	free(input);
 }
 
+/* Opens current entry while navigating. */
+static void
+nav_open(void)
+{
+	dir_entry_t *curr = get_current_entry(curr_view);
+	if(fentry_is_fake(curr))
+	{
+		return;
+	}
+
+	int is_dir_like = (fentry_is_dir(curr) || is_unc_root(curr_view->curr_dir));
+
+	CmdLineSubmode sub_mode = input_stat.sub_mode;
+	if(sub_mode == CLS_FILTER)
+	{
+		/* Accepting filter changes list of files, but in case of error it might be
+		 * better. */
+		local_filter_accept(curr_view, /*update_history=*/0);
+	}
+
+	if(is_dir_like)
+	{
+		char *initial = to_multibyte(input_stat.line);
+		if(rn_enter_dir(curr_view) == 0)
+		{
+			replace_string(&initial, "");
+		}
+
+		/* Auto-commands on entering a directory can do something that will require
+		 * a postponed view update.  Do it here, so that we don't work with file
+		 * list that's about to be changed.  In particular re-entering submode saves
+		 * top and cursor positions. */
+		switch(ui_view_query_scheduled_event(curr_view))
+		{
+			case UUE_NONE:
+				/* Nothing to do. */
+				break;
+			case UUE_REDRAW:
+				ui_view_schedule_redraw(curr_view);
+				break;
+			case UUE_RELOAD:
+				load_saving_pos(curr_view);
+				break;
+		}
+
+		if(initial != NULL)
+		{
+			enter_submode(sub_mode, initial, /*reenter=*/1);
+			nav_start(&input_stat);
+
+			free(initial);
+		}
+	}
+	else
+	{
+		leave_cmdline_mode(/*cancelled=*/0);
+		if(cfg.nav_open_files)
+		{
+			rn_open(curr_view, FHE_RUN);
+		}
+	}
+}
+
 /* Checks whether input line is empty.  Returns non-zero if so, otherwise
  * non-zero is returned. */
 static int
@@ -1525,8 +1706,9 @@ expand_abbrev(void)
 	const wchar_t *abbrev_rhs;
 	int no_remap;
 
-	/* No recursion on expanding abbreviations. */
-	if(input_stat.expanding_abbrev)
+	/* Don't expand command-line abbreviations in navigation and avoid recursion
+	 * on expanding abbreviations. */
+	if(input_stat.navigating || input_stat.expanding_abbrev)
 	{
 		return;
 	}
@@ -1620,7 +1802,7 @@ save_input_to_history(const keys_info_t *keys_info, const char input[])
 	}
 	else if(input_stat.sub_mode == CLS_PROMPT)
 	{
-		const int is_expr_reg = (input_stat.prev_mode == CMDLINE_MODE);
+		const int is_expr_reg = is_cmdmode(input_stat.prev_mode);
 		save_prompt_to_history(input, is_expr_reg);
 	}
 }
@@ -1641,12 +1823,12 @@ save_prompt_to_history(const char input[], int is_expr_reg)
 
 /* Performs final actions on successful querying of prompt input. */
 static void
-finish_prompt_submode(const char input[], prompt_cb cb)
+finish_prompt_submode(const char input[], prompt_cb cb, void *cb_arg)
 {
 	modes_post();
 	modes_pre();
 
-	cb(input);
+	cb(input, cb_arg);
 }
 
 /* Converts search command-line sub-mode to type of command input.  Returns
@@ -1725,37 +1907,28 @@ replace_wstring(wchar_t **str, const wchar_t with[])
 	return 0;
 }
 
+/* Recalls a newer historical entry or moves view cursor if navigating. */
 static void
 cmd_ctrl_n(key_info_t key_info, keys_info_t *keys_info)
 {
-	stop_completion();
-
-	if(input_stat.history_search == HIST_NONE)
-		save_users_input();
-
-	input_stat.history_search = HIST_GO;
-
-	hist_next(&input_stat, pick_hist(), cfg.history_len);
-}
+	if(input_stat.navigating)
+	{
+		if(curr_view->list_pos < curr_view->list_rows - 1)
+		{
+			move_view_cursor(&input_stat, curr_view, curr_view->list_pos + 1);
+		}
+		return;
+	}
 
-#ifdef ENABLE_EXTENDED_KEYS
-static void
-cmd_down(key_info_t key_info, keys_info_t *keys_info)
-{
 	stop_completion();
 
 	if(input_stat.history_search == HIST_NONE)
 		save_users_input();
 
-	if(input_stat.history_search != HIST_SEARCH)
-	{
-		input_stat.history_search = HIST_SEARCH;
-		input_stat.hist_search_len = input_stat.len;
-	}
+	input_stat.history_search = HIST_GO;
 
 	hist_next(&input_stat, pick_hist(), cfg.history_len);
 }
-#endif /* ENABLE_EXTENDED_KEYS */
 
 /* Puts next element in the history or restores user input if end of history has
  * been reached.  hist can be NULL, in which case nothing happens. */
@@ -1815,27 +1988,28 @@ cmd_ctrl_requals(key_info_t key_info, keys_info_t *keys_info)
 	input_stat.history_search = HIST_NONE;
 	stop_completion();
 
-	if(input_stat.prev_mode != CMDLINE_MODE)
+	if(!is_cmdmode(input_stat.prev_mode))
 	{
-		modcline_prompt("(=)", "", &expr_reg_prompt_cb, &expr_reg_prompt_completion,
-				/*allow_ee=*/1);
+		modcline_prompt("(=)", "", &expr_reg_prompt_cb, /*cb_arg=*/NULL,
+				&expr_reg_prompt_completion, /*allow_ee=*/1);
 	}
 }
 
 /* Handles result of expression register prompt. */
 static void
-expr_reg_prompt_cb(const char expr[])
+expr_reg_prompt_cb(const char expr[], void *arg)
 {
-	/* Try to parse expr, and convert the res to string if succeed. */
-	var_t res;
-	ParsingErrors parsing_error = parse(expr, 0, &res);
-	if(parsing_error != PE_NO_ERROR)
+	/* Try to parse expr and convert the result to string on success. */
+	parsing_result_t result = vle_parser_eval(expr, /*interactive=*/0);
+	if(result.error != PE_NO_ERROR)
 	{
+		/* TODO: maybe print error message on status bar. */
+		var_free(result.value);
 		return;
 	}
 
-	char *res_str = var_to_str(res);
-	var_free(res);
+	char *res_str = var_to_str(result.value);
+	var_free(result.value);
 
 	wchar_t *wide = to_wide_force(res_str);
 	free(res_str);
@@ -2127,7 +2301,7 @@ escape_cmd_for_pasting(const char str[])
 	wide_input[input_stat.index] = L'\0';
 	mb_input = to_multibyte(wide_input);
 
-	escaped = commands_escape_for_insertion(mb_input, strlen(mb_input), str);
+	escaped = cmds_insertion_escape(mb_input, strlen(mb_input), str);
 
 	free(mb_input);
 	free(wide_input);
@@ -2341,55 +2515,6 @@ find_next_word(void)
 	}
 }
 
-static void
-cmd_left(key_info_t key_info, keys_info_t *keys_info)
-{
-	input_stat.history_search = HIST_NONE;
-	stop_completion();
-
-	if(input_stat.index > 0)
-	{
-		input_stat.index--;
-		input_stat.curs_pos -= esc_wcwidth(input_stat.line[input_stat.index]);
-		update_cursor();
-	}
-}
-
-static void
-cmd_right(key_info_t key_info, keys_info_t *keys_info)
-{
-	input_stat.history_search = HIST_NONE;
-	stop_completion();
-
-	if(input_stat.index < input_stat.len)
-	{
-		input_stat.curs_pos += esc_wcwidth(input_stat.line[input_stat.index]);
-		input_stat.index++;
-		update_cursor();
-	}
-}
-
-static void
-cmd_home(key_info_t key_info, keys_info_t *keys_info)
-{
-	input_stat.index = 0;
-	input_stat.curs_pos = input_stat.prompt_wid;
-	update_cursor();
-}
-
-/* Moves cursor to the end of command-line on Ctrl+E and End. */
-static void
-cmd_end(key_info_t key_info, keys_info_t *keys_info)
-{
-	if(input_stat.index == input_stat.len)
-		return;
-
-	input_stat.index = input_stat.len;
-	input_stat.curs_pos = input_stat.prompt_wid +
-			esc_wcswidth(input_stat.line, (size_t)-1);
-	update_cursor();
-}
-
 static void
 cmd_delete(key_info_t key_info, keys_info_t *keys_info)
 {
@@ -2429,9 +2554,39 @@ replace_input_line(line_stats_t *stat, const char new[])
 	return 0;
 }
 
+/* Goes to parent directory when in navigation. */
+static void
+cmd_ctrl_o(key_info_t key_info, keys_info_t *keys_info)
+{
+	if(!input_stat.navigating)
+	{
+		return;
+	}
+
+	CmdLineSubmode sub_mode = input_stat.sub_mode;
+	if(sub_mode == CLS_FILTER)
+	{
+		local_filter_accept(curr_view, /*update_history=*/0);
+	}
+
+	rn_leave(curr_view, /*levels=*/1);
+	enter_submode(sub_mode, /*initial=*/"", /*reenter=*/1);
+	nav_start(&input_stat);
+}
+
+/* Recalls an older historical entry or moves view cursor if navigating. */
 static void
 cmd_ctrl_p(key_info_t key_info, keys_info_t *keys_info)
 {
+	if(input_stat.navigating)
+	{
+		if(curr_view->list_pos > 0)
+		{
+			move_view_cursor(&input_stat, curr_view, curr_view->list_pos - 1);
+		}
+		return;
+	}
+
 	stop_completion();
 
 	if(input_stat.history_search == HIST_NONE)
@@ -2442,6 +2597,22 @@ cmd_ctrl_p(key_info_t key_info, keys_info_t *keys_info)
 	hist_prev(&input_stat, pick_hist(), cfg.history_len);
 }
 
+/* Moves cursor in the view and updates information related to its position. */
+static void
+move_view_cursor(line_stats_t *stat, view_t *view, int new_pos)
+{
+	fpos_set_pos(view, new_pos);
+
+	if(stat->sub_mode == CLS_FILTER)
+	{
+		local_filter_update_pos(view);
+	}
+	else
+	{
+		save_view_port(stat, view);
+	}
+}
+
 /* Swaps the order of two characters. */
 static void
 cmd_ctrl_t(key_info_t key_info, keys_info_t *keys_info)
@@ -2474,10 +2645,103 @@ cmd_ctrl_t(key_info_t key_info, keys_info_t *keys_info)
 	update_cmdline_text(&input_stat);
 }
 
+/* Toggles navigation for search/filtering. */
+static void
+cmd_ctrl_y(key_info_t key_info, keys_info_t *keys_info)
+{
+	if(!input_stat.search_mode && input_stat.sub_mode != CLS_FILTER)
+	{
+		return;
+	}
+	if(!cfg.inc_search || input_stat.prev_mode != NORMAL_MODE)
+	{
+		return;
+	}
+
+	if(!input_stat.navigating)
+	{
+		nav_start(&input_stat);
+	}
+	else
+	{
+		nav_stop(&input_stat);
+	}
+	draw_cmdline_text(&input_stat);
+}
+
+/* Enables navigation. */
+static void
+nav_start(line_stats_t *stat)
+{
+	stat->navigating = 1;
+
+	size_t nav_prefix_len = wcslen(NAV_PREFIX);
+
+	wchar_t new_prompt[NAME_MAX + 1] = NAV_PREFIX;
+	wcsncpy(new_prompt + nav_prefix_len, stat->prompt,
+			ARRAY_LEN(new_prompt) - nav_prefix_len - 1);
+
+	wcscpy(stat->prompt, new_prompt);
+	stat->prompt_wid += nav_prefix_len;
+	stat->curs_pos += nav_prefix_len;
+
+	vle_mode_set(NAV_MODE, VMT_SECONDARY);
+}
+
+/* Disables navigation. */
+static void
+nav_stop(line_stats_t *stat)
+{
+	stat->navigating = 0;
+
+	size_t nav_prefix_len = wcslen(NAV_PREFIX);
+
+	memmove(stat->prompt, &stat->prompt[nav_prefix_len],
+			sizeof(stat->prompt) - sizeof(wchar_t)*nav_prefix_len);
+	stat->prompt_wid -= nav_prefix_len;
+	stat->curs_pos -= nav_prefix_len;
+
+	vle_mode_set(CMDLINE_MODE, VMT_SECONDARY);
+}
+
 #ifdef ENABLE_EXTENDED_KEYS
+
+/* Fetches a matching future historical entry or moves view cursor down in
+ * navigation. */
+static void
+cmd_down(key_info_t key_info, keys_info_t *keys_info)
+{
+	if(input_stat.navigating)
+	{
+		cmd_ctrl_n(key_info, keys_info);
+		return;
+	}
+
+	stop_completion();
+
+	if(input_stat.history_search == HIST_NONE)
+		save_users_input();
+
+	if(input_stat.history_search != HIST_SEARCH)
+	{
+		input_stat.history_search = HIST_SEARCH;
+		input_stat.hist_search_len = input_stat.len;
+	}
+
+	hist_next(&input_stat, pick_hist(), cfg.history_len);
+}
+
+/* Fetches a matching past historical entry or moves view cursor up in
+ * navigation. */
 static void
 cmd_up(key_info_t key_info, keys_info_t *keys_info)
 {
+	if(input_stat.navigating)
+	{
+		cmd_ctrl_p(key_info, keys_info);
+		return;
+	}
+
 	stop_completion();
 
 	if(input_stat.history_search == HIST_NONE)
@@ -2491,6 +2755,87 @@ cmd_up(key_info_t key_info, keys_info_t *keys_info)
 
 	hist_prev(&input_stat, pick_hist(), cfg.history_len);
 }
+
+/* Moves command-line cursor to the left or goes to parent directory in
+ * navigation. */
+static void
+cmd_left(key_info_t key_info, keys_info_t *keys_info)
+{
+	if(input_stat.navigating)
+	{
+		cmd_ctrl_o(key_info, keys_info);
+	}
+	else
+	{
+		cmd_ctrl_b(key_info, keys_info);
+	}
+}
+
+/* Moves command-line cursor to the right or enter active entry in
+ * navigation. */
+static void
+cmd_right(key_info_t key_info, keys_info_t *keys_info)
+{
+	if(input_stat.navigating)
+	{
+		cmd_return(key_info, keys_info);
+	}
+	else
+	{
+		cmd_ctrl_f(key_info, keys_info);
+	}
+}
+
+/* Moves command-line cursor to the beginning of command-line or moves
+ * view cursor to the first file in navigation. */
+static void
+cmd_home(key_info_t key_info, keys_info_t *keys_info)
+{
+	if(input_stat.navigating)
+	{
+		fpos_set_pos(curr_view, 0);
+	}
+	else
+	{
+		cmd_ctrl_a(key_info, keys_info);
+	}
+}
+
+/* Moves command-line cursor to the end of command-line or moves view cursor to
+ * the last file in navigation. */
+static void
+cmd_end(key_info_t key_info, keys_info_t *keys_info)
+{
+	if(input_stat.navigating)
+	{
+		fpos_set_pos(curr_view, curr_view->list_rows - 1);
+	}
+	else
+	{
+		cmd_ctrl_e(key_info, keys_info);
+	}
+}
+
+/* Scrolls view up in navigation. */
+static void
+cmd_page_up(key_info_t key_info, keys_info_t *keys_info)
+{
+	if(input_stat.navigating)
+	{
+		fview_scroll_page_up(curr_view);
+	}
+}
+
+/* Scrolls view down in navigation. */
+static void
+cmd_page_down(key_info_t key_info, keys_info_t *keys_info)
+{
+	if(input_stat.navigating)
+	{
+		fview_scroll_page_down(curr_view);
+	}
+}
+
 #endif /* ENABLE_EXTENDED_KEYS */
 
 /* Puts previous element in the history.  hist can be NULL, in which case
@@ -2570,7 +2915,7 @@ pick_hist(void)
 	}
 	if(input_stat.sub_mode == CLS_PROMPT)
 	{
-		if(input_stat.prev_mode == CMDLINE_MODE)
+		if(is_cmdmode(input_stat.prev_mode))
 		{
 			return &curr_stats.exprreg_hist;
 		}
@@ -2583,6 +2928,13 @@ pick_hist(void)
 	return NULL;
 }
 
+/* Checks for a command-line-like mode. */
+static int
+is_cmdmode(int mode)
+{
+  return (mode == CMDLINE_MODE || mode == NAV_MODE);
+}
+
 /* Updates command-line properties and redraws it. */
 static void
 update_cmdline(line_stats_t *stat)
@@ -2711,12 +3063,11 @@ line_completion(line_stats_t *stat)
 		vle_compl_reset();
 
 		compl_func_arg = CPP_NONE;
-		if(input_stat.sub_mode == CLS_COMMAND ||
-				input_stat.sub_mode == CLS_MENU_COMMAND)
+		if(stat->sub_mode == CLS_COMMAND || stat->sub_mode == CLS_MENU_COMMAND)
 		{
-			line_mb_cmd = find_last_command(line_mb);
+			line_mb_cmd = cmds_find_last(line_mb);
 
-			const CmdLineLocation ipt = get_cmdline_location(line_mb,
+			const CmdLineLocation ipt = cmds_classify_pos(line_mb,
 					line_mb + strlen(line_mb));
 			switch(ipt)
 			{
@@ -2771,7 +3122,7 @@ static char *
 escaped_arg_hook(const char match[])
 {
 #ifndef _WIN32
-	return shell_like_escape(match, 1);
+	return posix_like_escape(match, /*type=*/1);
 #else
 	return strdup(escape_for_cd(match));
 #endif
@@ -2887,11 +3238,70 @@ stop_regular_completion(void)
 	}
 }
 
-line_stats_t *
+int
+modcline_complete_dirs(const char str[], void *arg)
+{
+	int name_offset = after_last(str, '/') - str;
+	return name_offset
+	     + filename_completion(str, CT_DIRONLY, /*skip_canonicalization=*/0);
+}
+
+int
+modcline_complete_files(const char str[], void *arg)
+{
+	int name_offset = after_last(str, '/') - str;
+	return name_offset
+	     + filename_completion(str, CT_ALL, /*skip_canonicalization=*/0);
+}
+
+TSTATIC line_stats_t *
 get_line_stats(void)
 {
 	return &input_stat;
 }
 
+/* Processes events from the mouse. */
+static void
+handle_mouse_event(key_info_t key_info, keys_info_t *keys_info)
+{
+	MEVENT e;
+	if(ui_get_mouse(&e) != OK)
+	{
+		return;
+	}
+
+	if(!wenclose(status_bar, e.y, e.x))
+	{
+		return;
+	}
+
+	if(e.bstate & BUTTON1_PRESSED)
+	{
+		wmouse_trafo(status_bar, &e.y, &e.x, FALSE);
+
+		int idx = e.y*getmaxx(status_bar) + e.x - input_stat.prompt_wid;
+		if(idx < 0)
+		{
+			idx = 0;
+		}
+		else if(idx > input_stat.len)
+		{
+			idx = input_stat.len;
+		}
+
+		input_stat.index = idx;
+		input_stat.curs_pos = input_stat.index + input_stat.prompt_wid;
+		update_cmdline_text(&input_stat);
+	}
+	else if(e.bstate & BUTTON4_PRESSED)
+	{
+		cmd_ctrl_p(key_info, keys_info);
+	}
+	else if(e.bstate & (BUTTON2_PRESSED | BUTTON5_PRESSED))
+	{
+		cmd_ctrl_n(key_info, keys_info);
+	}
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 filetype=c : */
diff --git a/src/modes/cmdline.h b/src/modes/cmdline.h
index 7a90b5b..55405a0 100644
--- a/src/modes/cmdline.h
+++ b/src/modes/cmdline.h
@@ -40,8 +40,11 @@ typedef enum
 }
 CmdLineSubmode;
 
-/* Callback for prompt input. */
-typedef void (*prompt_cb)(const char response[]);
+struct menu_data_t;
+
+/* Callback for prompt input.  Invoked with NULL on cancellation.  arg is user
+ * supplied value, which is passed through. */
+typedef void (*prompt_cb)(const char response[], void *arg);
 
 /* Custom prompt line completion function.  arg is user supplied value, which is
  * passed through.  Should return completion offset. */
@@ -50,39 +53,54 @@ typedef int (*complete_cmd_func)(const char cmd[], void *arg);
 /* Initializes command-line mode. */
 void modcline_init(void);
 
-/* Enters command-line editing mode with specified submode.  cmd specifies
- * initial value, ptr is submode-specific data to be passed back. */
-void modcline_enter(CmdLineSubmode sub_mode, const char cmd[], void *ptr);
+/* Initializes navigation mode, which is nested into the command-line mode. */
+void modnav_init(void);
+
+/* Enters command-line editing mode with specified submode.  initial is the
+ * start value. */
+void modcline_enter(CmdLineSubmode sub_mode, const char initial[]);
 
-/* Enters command-line editing mode with prompt submode activated.  cmd
- * specifies initial value, cb is callback called on success, complete is
- * completion function, allow_ee specifies whether issuing external editor is
- * allowed. */
-void modcline_prompt(const char prompt[], const char cmd[], prompt_cb cb,
-		complete_cmd_func complete, int allow_ee);
+/* Version of modcline_enter() specific to CLS_MENU_* submodes. */
+void modcline_in_menu(CmdLineSubmode sub_mode, struct menu_data_t *m);
+
+/* Enters command-line editing mode with prompt submode activated.  initial is
+ * the start value, cb is callback called with the result on success and
+ * with NULL on cancellation, complete is completion function (can be NULL),
+ * allow_ee specifies whether issuing external editor is allowed. */
+void modcline_prompt(const char prompt[], const char initial[], prompt_cb cb,
+		void *cb_arg, complete_cmd_func complete, int allow_ee);
 
 /* Redraws UI elements of the command-line mode. */
 void modcline_redraw(void);
 
-#ifdef TEST
+/* Completes paths ignoring files.  Returns completion offset. */
+int modcline_complete_dirs(const char str[], void *arg);
+
+/* Completes paths to both files and directories.  Returns completion offset. */
+int modcline_complete_files(const char str[], void *arg);
+
+#if defined(TEST) || defined(CMDLINE_IMPL)
+
 #include <stddef.h> /* size_t wchar_t */
 
 #include "../compat/fs_limits.h"
 #include "../utils/hist.h"
 
+/* History search mode. */
 typedef enum
 {
-	HIST_NONE,
-	HIST_GO,
-	HIST_SEARCH
+	HIST_NONE,   /* No search in history is active. */
+	HIST_GO,     /* Retrieving items from history one by one. */
+	HIST_SEARCH, /* Retrieving items that match entered prefix skipping others. */
 }
 HIST;
 
+/* Describes possible states of a prompt used for interactive search. */
 typedef enum
 {
-	PS_NORMAL,
-	PS_WRONG_PATTERN,
-	PS_NO_MATCH,
+	PS_NORMAL,        /* Normal state (empty input or input is OK). */
+	PS_WRONG_PATTERN, /* Pattern contains a mistake. */
+	PS_NO_MATCH,      /* Pattern is OK, but no matches found. */
 }
 PromptState;
 
@@ -95,13 +113,19 @@ typedef struct
 	int prev_mode;
 	/* Kind of command-line mode. */
 	CmdLineSubmode sub_mode;
+	/* Whether performing quick navigation. */
+	int navigating;
 	/* Whether current submode allows external editing. */
 	int sub_mode_allows_ee;
-	/* Extra parameter for submode-related calls. */
-	void *sub_mode_ptr;
+	/* CLS_MENU_*-specific data. */
+	struct menu_data_t *menu;
+	/* CLS_PROMPT-specific data. */
+	prompt_cb prompt_callback;
+	void *prompt_callback_arg;
 
 	/* Line editing state. */
 	wchar_t *line;                /* The line reading. */
+	wchar_t *last_line;           /* Previous contents of the line. */
 	wchar_t *initial_line;        /* Initial state of the line. */
 	int index;                    /* Index of the current character in cmdline. */
 	int curs_pos;                 /* Position of the cursor in status bar. */
@@ -127,9 +151,10 @@ typedef struct
 	wchar_t *line_buf;   /* Content of line before using history. */
 
 	/* For search prompt. */
-	int search_mode; /* If it's a search prompt. */
-	int old_top;     /* Saved top for interactive searching. */
-	int old_pos;     /* Saved position for interactive searching. */
+	int search_mode;        /* If it's a search prompt. */
+	int search_match_found; /* Reflects interactive search success/failure. */
+	int old_top;            /* Saved top for interactive searching. */
+	int old_pos;            /* Saved position for interactive searching. */
 
 	/* Other state. */
 	int line_edited;         /* Cache for whether input line changed flag. */
@@ -138,7 +163,9 @@ typedef struct
 	PromptState state;       /* Prompt state with regard to current input. */
 }
 line_stats_t;
+
 #endif
+
 TSTATIC_DEFS(
 	int line_completion(line_stats_t *stat);
 	const wchar_t * extract_abbrev(line_stats_t *stat, int *pos, int *no_remap);
diff --git a/src/modes/dialogs/attr_dialog_nix.c b/src/modes/dialogs/attr_dialog_nix.c
index 0ce41a3..0435199 100644
--- a/src/modes/dialogs/attr_dialog_nix.c
+++ b/src/modes/dialogs/attr_dialog_nix.c
@@ -180,7 +180,7 @@ enter_attr_mode(view_t *active_view)
 
 	vle_mode_set(ATTR_MODE, VMT_SECONDARY);
 	ui_hide_graphics();
-	clear_input_bar();
+	modes_input_bar_clear();
 	curr_stats.use_input_bar = 0;
 
 	/* Values:
@@ -270,7 +270,7 @@ redraw_attr_dialog(void)
 	free(title);
 
 	checked_wmove(change_win, curr, col);
-	curs_set(1);
+	ui_set_cursor(/*visibility=*/1);
 	ui_refresh_win(change_win);
 }
 
@@ -371,7 +371,7 @@ static void
 leave_attr_mode(void)
 {
 	vle_mode_set(NORMAL_MODE, VMT_PRIMARY);
-	curs_set(0);
+	ui_set_cursor(/*visibility=*/0);
 	curr_stats.use_input_bar = 1;
 
 	flist_sel_stash(view);
diff --git a/src/modes/dialogs/attr_dialog_win.c b/src/modes/dialogs/attr_dialog_win.c
index 7c4a1fd..8a6e160 100644
--- a/src/modes/dialogs/attr_dialog_win.c
+++ b/src/modes/dialogs/attr_dialog_win.c
@@ -163,7 +163,7 @@ enter_attr_mode(view_t *active_view)
 	view = active_view;
 	vle_mode_set(ATTR_MODE, VMT_SECONDARY);
 	ui_hide_graphics();
-	clear_input_bar();
+	modes_input_bar_clear();
 	curr_stats.use_input_bar = 0;
 
 	init();
diff --git a/src/modes/dialogs/change_dialog.c b/src/modes/dialogs/change_dialog.c
index 9de338b..b585f7d 100644
--- a/src/modes/dialogs/change_dialog.c
+++ b/src/modes/dialogs/change_dialog.c
@@ -31,6 +31,7 @@
 #include "../../ui/colors.h"
 #include "../../ui/ui.h"
 #include "../../utils/macros.h"
+#include "../../utils/utils.h"
 #include "../../fops_misc.h"
 #include "../../fops_rename.h"
 #include "../../status.h"
@@ -104,7 +105,7 @@ enter_change_mode(view_t *active_view)
 	vle_mode_set(CHANGE_MODE, VMT_SECONDARY);
 	ui_hide_graphics();
 
-	curs_set(0);
+	ui_set_cursor(/*visibility=*/0);
 	update_all_windows();
 
 	top = 2;
@@ -198,20 +199,14 @@ cmd_G(key_info_t key_info, keys_info_t *keys_info)
 static void
 cmd_gg(key_info_t key_info, keys_info_t *keys_info)
 {
-	if(key_info.count == NO_COUNT_GIVEN)
-		goto_line(1);
-	else
-		goto_line(key_info.count);
+	goto_line(def_count(key_info.count));
 }
 
 static void
 cmd_j(key_info_t key_info, keys_info_t *keys_info)
 {
-	if(key_info.count == NO_COUNT_GIVEN)
-		key_info.count = 1;
-
 	clear_at_pos();
-	curr += key_info.count*step;
+	curr += def_count(key_info.count)*step;
 	if(curr > bottom)
 		curr = bottom;
 
@@ -222,11 +217,8 @@ cmd_j(key_info_t key_info, keys_info_t *keys_info)
 static void
 cmd_k(key_info_t key_info, keys_info_t *keys_info)
 {
-	if(key_info.count == NO_COUNT_GIVEN)
-		key_info.count = 1;
-
 	clear_at_pos();
-	curr -= key_info.count*step;
+	curr -= def_count(key_info.count)*step;
 	if(curr < top)
 		curr = top;
 
diff --git a/src/modes/dialogs/msg_dialog.c b/src/modes/dialogs/msg_dialog.c
index b4770f3..1f601e0 100644
--- a/src/modes/dialogs/msg_dialog.c
+++ b/src/modes/dialogs/msg_dialog.c
@@ -41,13 +41,18 @@
 #include "../../status.h"
 #include "../wk.h"
 
+/* Both vertical and horizontal margin for the dialog. */
+#define MARGIN 1
+
 /* Kinds of dialogs. */
 typedef enum
 {
-	D_ERROR,              /* Error message. */
-	D_QUERY_WITHOUT_LIST, /* User query with all lines centered. */
-	D_QUERY_WITH_LIST,    /* User query with left aligned list and one line
-	                         centered at the top. */
+	D_ERROR,              /* Error message.  All lines are left-aligned unless
+	                         message is a single line that doesn't wrap, in which
+	                         case it's centered. */
+	D_QUERY_CENTER_EACH,  /* Query with each line centered on its own. */
+	D_QUERY_CENTER_FIRST, /* Query with the first line centered. */
+	D_QUERY_CENTER_BLOCK, /* Query with all lines centered as a block. */
 }
 Dialog;
 
@@ -62,30 +67,46 @@ typedef enum
 }
 DialogResult;
 
+/* Internal information about a prompt. */
+typedef struct
+{
+	const custom_prompt_t details; /* Essential parts of prompt description. */
+
+	/* Configuration. */
+	const Dialog kind;     /* Internal dialog kind. */
+	const int prompt_skip; /* Whether to allow skipping future errors. */
+	const int accept_mask; /* Mask of allowed DR_* results. */
+
+	/* State. */
+	int exit_requested;  /* Main loop quit flag. */
+	DialogResult result; /* Dialog result. */
+	char custom_result;  /* One of user-defined input keys. */
+}
+dialog_data_t;
+
 static int def_handler(wchar_t key);
 static void cmd_ctrl_c(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_ctrl_l(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_ctrl_m(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_n(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_y(key_info_t key_info, keys_info_t *keys_info);
-static void handle_response(DialogResult dr);
-static void leave(DialogResult dr);
+static void handle_response(dialog_data_t *data, DialogResult dr);
+static void leave(dialog_data_t *data, DialogResult dr, char custom_result);
 static int prompt_error_msg_internalv(const char title[], const char format[],
 		int prompt_skip, va_list pa);
 static int prompt_error_msg_internal(const char title[], const char message[],
 		int prompt_skip);
-static void prompt_msg_internal(const char title[], const char message[],
-		const response_variant variants[], int with_list);
-static void enter(const char title[], const char message[], int prompt_skip,
-		int result_mask);
-static void redraw_error_msg(const char title_arg[], const char message_arg[],
-		int prompt_skip, int lazy);
-static const char * get_control_msg(Dialog msg_kind, int global_skip);
+static void prompt_msg_internal(dialog_data_t *data);
+static void enter(dialog_data_t *data);
+static void redraw_error_msg(const dialog_data_t *data, int lazy);
+static const char * get_control_msg(const dialog_data_t *data);
 static const char * get_custom_control_msg(const response_variant responses[]);
 static void draw_msg(const char title[], const char msg[],
-		const char ctrl_msg[], int lines_to_center, int recommended_width);
-static size_t measure_sub_lines(const char msg[], size_t *max_len);
-static size_t determine_width(const char msg[]);
+		const char ctrl_msg[], int lines_to_center, int block_center,
+		int recommended_width);
+static size_t measure_sub_lines(const char msg[], int skip_empty,
+		size_t *max_len);
+static size_t measure_text_width(const char msg[]);
 
 /* List of builtin key bindings. */
 static keys_add_info_t builtin_cmds[] = {
@@ -97,19 +118,8 @@ static keys_add_info_t builtin_cmds[] = {
 	{WK_y,   {{&cmd_y},      .descr = "confirm the query"}},
 };
 
-/* Main loop quit flag. */
-static int quit;
-/* Dialog result. */
-static DialogResult dialog_result;
-/* Bit mask of R_* kinds of results that are allowed. */
-static int accept_mask;
-/* Type of active dialog message. */
-static Dialog msg_kind;
-
-/* Possible responses for custom prompt message. */
-static const response_variant *responses;
-/* One of user-defined input keys. */
-static char custom_result;
+/* Currently active dialog or NULL. */
+static dialog_data_t *current_dialog;
 
 void
 init_msg_dialog_mode(void)
@@ -134,13 +144,12 @@ def_handler(wchar_t key)
 		return 0;
 	}
 
-	const response_variant *response = responses;
+	const response_variant *response = current_dialog->details.variants;
 	while(response != NULL && response->key != '\0')
 	{
 		if(response->key == (char)key)
 		{
-			custom_result = key;
-			leave(DR_CUSTOM);
+			leave(current_dialog, DR_CUSTOM, key);
 			break;
 		}
 		++response;
@@ -152,7 +161,7 @@ def_handler(wchar_t key)
 static void
 cmd_ctrl_c(key_info_t key_info, keys_info_t *keys_info)
 {
-	handle_response(DR_CANCEL);
+	handle_response(current_dialog, DR_CANCEL);
 }
 
 /* Redraws the screen. */
@@ -166,27 +175,27 @@ cmd_ctrl_l(key_info_t key_info, keys_info_t *keys_info)
 static void
 cmd_ctrl_m(key_info_t key_info, keys_info_t *keys_info)
 {
-	handle_response(DR_OK);
+	handle_response(current_dialog, DR_OK);
 }
 
 /* Denies the query. */
 static void
 cmd_n(key_info_t key_info, keys_info_t *keys_info)
 {
-	handle_response(DR_NO);
+	handle_response(current_dialog, DR_NO);
 }
 
 /* Confirms the query. */
 static void
 cmd_y(key_info_t key_info, keys_info_t *keys_info)
 {
-	handle_response(DR_YES);
+	handle_response(current_dialog, DR_YES);
 }
 
 /* Processes users choice.  Leaves the mode if the result is found among the
  * list of expected results. */
 static void
-handle_response(DialogResult dr)
+handle_response(dialog_data_t *data, DialogResult dr)
 {
 	/* Map result to corresponding input key to omit branching per handler. */
 	static const char r_to_c[] = {
@@ -198,27 +207,28 @@ handle_response(DialogResult dr)
 
 	(void)def_handler(r_to_c[dr]);
 	/* Default handler might have already requested quitting. */
-	if(!quit)
+	if(!data->exit_requested)
 	{
-		if(accept_mask & MASK(dr))
+		if(data->accept_mask & MASK(dr))
 		{
-			leave(dr);
+			leave(data, dr, /*custom_result=*/'?');
 		}
 	}
 }
 
 /* Leaves the mode with given result. */
 static void
-leave(DialogResult dr)
+leave(dialog_data_t *data, DialogResult dr, char custom_result)
 {
-	dialog_result = dr;
-	quit = 1;
+	data->result = dr;
+	data->custom_result = custom_result;
+	data->exit_requested = 1;
 }
 
 void
 redraw_msg_dialog(int lazy)
 {
-	redraw_error_msg(NULL, NULL, 0, lazy);
+	redraw_error_msg(/*data=*/NULL, lazy);
 }
 
 void
@@ -299,26 +309,40 @@ prompt_error_msg_internal(const char title[], const char message[],
 		return 0;
 	}
 
-	msg_kind = D_ERROR;
-
-	enter(title, message, prompt_skip,
-			MASK(DR_OK) | (prompt_skip ? MASK(DR_CANCEL) : 0));
+	dialog_data_t data = {
+		.details = {
+			.title = title,
+			.message = message,
+		},
+		.kind = D_ERROR,
+		.prompt_skip = prompt_skip,
+		.accept_mask = MASK(DR_OK) | (prompt_skip ? MASK(DR_CANCEL) : 0),
+	};
+	enter(&data);
 
 	if(curr_stats.load_stage < 2)
 	{
-		skip_until_started = (dialog_result == DR_CANCEL);
+		skip_until_started = (data.result == DR_CANCEL);
 	}
 
 	modes_redraw();
 
-	return (dialog_result == DR_CANCEL);
+	return (data.result == DR_CANCEL);
 }
 
 int
 prompt_msg(const char title[], const char message[])
 {
-	prompt_msg_internal(title, message, NULL, 0);
-	return (dialog_result == DR_YES);
+	dialog_data_t data = {
+		.details = {
+			.title = title,
+			.message = message,
+		},
+		.kind = D_QUERY_CENTER_EACH,
+		.accept_mask = MASK(DR_YES, DR_NO),
+	};
+	prompt_msg_internal(&data);
+	return (data.result == DR_YES);
 }
 
 int
@@ -336,34 +360,34 @@ prompt_msgf(const char title[], const char format[], ...)
 }
 
 char
-prompt_msg_custom(const char title[], const char message[],
-		const response_variant variants[])
+prompt_msg_custom(const custom_prompt_t *details)
 {
-	assert(variants[0].key != '\0' && "Variants should have at least one item.");
-	prompt_msg_internal(title, message, variants, 0);
-	return custom_result;
+	assert(details->variants[0].key != '\0' &&
+			"Variants should have at least one item.");
+
+	dialog_data_t data = {
+		.details = *details,
+		.kind = details->block_center ? D_QUERY_CENTER_BLOCK : D_QUERY_CENTER_EACH,
+	};
+
+	prompt_msg_internal(&data);
+	return data.custom_result;
 }
 
-/* Common implementation of prompt message.  The variants can be NULL. */
+/* Common implementation of prompt message. */
 static void
-prompt_msg_internal(const char title[], const char message[],
-		const response_variant variants[], int with_list)
+prompt_msg_internal(dialog_data_t *data)
 {
-	responses = variants;
-	msg_kind = (with_list ? D_QUERY_WITH_LIST : D_QUERY_WITHOUT_LIST);
-
-	enter(title, message, /*prompt_skip=*/0,
-			variants == NULL ? MASK(DR_YES, DR_NO) : 0);
-
+	enter(data);
 	modes_redraw();
 }
 
 /* Enters the mode, which won't be left until one of expected results specified
  * by the mask is picked by the user. */
 static void
-enter(const char title[], const char message[], int prompt_skip,
-		int result_mask)
+enter(dialog_data_t *data)
 {
+	dialog_data_t *prev_dialog = current_dialog;
 	const int prev_use_input_bar = curr_stats.use_input_bar;
 	const vle_mode_t prev_mode = vle_mode_get();
 
@@ -374,55 +398,68 @@ enter(const char title[], const char message[], int prompt_skip,
 
 	ui_hide_graphics();
 
-	accept_mask = result_mask;
+	current_dialog = data;
 	curr_stats.use_input_bar = 0;
 	vle_mode_set(MSG_MODE, VMT_SECONDARY);
 
-	redraw_error_msg(title, message, prompt_skip, /*lazy=*/0);
+	redraw_error_msg(data, /*lazy=*/0);
 
-	quit = 0;
+	data->exit_requested = 0;
 	/* Avoid starting nested loop in tests. */
 	if(curr_stats.load_stage > 0)
 	{
-		event_loop(&quit, /*manage_marking=*/0);
+		event_loop(&data->exit_requested, /*manage_marking=*/0);
 	}
 
 	vle_mode_set(prev_mode, VMT_SECONDARY);
 	curr_stats.use_input_bar = prev_use_input_bar;
+	current_dialog = prev_dialog;
 }
 
 /* Draws error message on the screen or redraws the last message when both
  * title_arg and message_arg are NULL. */
 static void
-redraw_error_msg(const char title_arg[], const char message_arg[],
-		int prompt_skip, int lazy)
+redraw_error_msg(const dialog_data_t *data, int lazy)
 {
-	/* TODO: refactor this function redraw_error_msg() */
+	static const dialog_data_t *last_data;
 
-	static const char *title;
-	static const char *message;
-	static int ctrl_c;
-
-	const char *ctrl_msg;
-	const int lines_to_center = msg_kind == D_QUERY_WITHOUT_LIST ? INT_MAX
-	                          : msg_kind == D_QUERY_WITH_LIST ? 1 : 0;
-
-	if(title_arg != NULL && message_arg != NULL)
+	if(data == NULL)
+	{
+		data = last_data;
+	}
+	else
 	{
-		title = title_arg;
-		message = message_arg;
-		ctrl_c = prompt_skip;
+		last_data = data;
 	}
+	assert(data != NULL && "Invalid dialog redraw request!");
 
-	if(title == NULL || message == NULL)
+	const char *message = data->details.message;
+	char *free_me = NULL;
+	if(data->details.make_message != NULL)
 	{
-		assert(title != NULL && "Asked to redraw dialog, but no title is set.");
-		assert(message != NULL && "Asked to redraw dialog, but no message is set.");
-		return;
+		int sw, sh;
+		getmaxyx(stdscr, sh, sw);
+		int max_w = sw - 2 - 2 - MARGIN*2;
+		int max_h = sh - 2 - ui_stat_height() - 2 - MARGIN*2;
+
+		free_me = data->details.make_message(max_w, max_h, data->details.user_data);
+		if(free_me == NULL)
+		{
+			return;
+		}
+
+		message = free_me;
 	}
 
-	ctrl_msg = get_control_msg(msg_kind, ctrl_c);
-	draw_msg(title, message, ctrl_msg, lines_to_center, 0);
+	const char *ctrl_msg = get_control_msg(data);
+	int lines_to_center = (data->kind == D_QUERY_CENTER_EACH) ? INT_MAX
+	                    : (data->kind == D_QUERY_CENTER_BLOCK) ? INT_MAX
+	                    : (data->kind == D_QUERY_CENTER_FIRST) ? 1
+	                    : 0;
+	draw_msg(data->details.title, message, ctrl_msg, lines_to_center,
+			data->details.block_center, /*recommended_width=*/0);
+
+	free(free_me);
 
 	if(lazy)
 	{
@@ -435,20 +472,21 @@ redraw_error_msg(const char title_arg[], const char message_arg[],
 }
 
 /* Picks control message (information on available actions) basing on dialog
- * kind and previous actions.  Returns the message. */
+ * kind.  Returns the message. */
 static const char *
-get_control_msg(Dialog msg_kind, int global_skip)
+get_control_msg(const dialog_data_t *data)
 {
-	if(msg_kind == D_QUERY_WITHOUT_LIST || msg_kind == D_QUERY_WITH_LIST)
+	if(data->kind != D_ERROR)
 	{
-		if(responses == NULL)
+		if(data->details.variants == NULL)
 		{
 			return "Enter [y]es or [n]o";
 		}
 
-		return get_custom_control_msg(responses);
+		return get_custom_control_msg(data->details.variants);
 	}
-	else if(global_skip)
+
+	if(data->prompt_skip)
 	{
 		return "Press Return to continue or "
 		       "Ctrl-C to skip its future error messages";
@@ -472,10 +510,6 @@ get_custom_control_msg(const response_variant responses[])
 		if(response->descr[0] != '\0')
 		{
 			(void)sstrappend(msg_buf, &len, sizeof(msg_buf), response->descr);
-			if(response[1].key != '\0')
-			{
-				(void)sstrappendch(msg_buf, &len, sizeof(msg_buf), '/');
-			}
 		}
 
 		++response;
@@ -495,7 +529,8 @@ draw_msgf(const char title[], const char ctrl_msg[], int recommended_width,
 	vsnprintf(msg, sizeof(msg), format, pa);
 	va_end(pa);
 
-	draw_msg(title, msg, ctrl_msg, 0, recommended_width);
+	draw_msg(title, msg, ctrl_msg, /*lines_to_center=*/0, /*block_center=*/0,
+			recommended_width);
 	touch_all_windows();
 	ui_refresh_win(error_win);
 }
@@ -504,119 +539,130 @@ draw_msgf(const char title[], const char ctrl_msg[], int recommended_width,
  * specified title and control message on error_win. */
 static void
 draw_msg(const char title[], const char msg[], const char ctrl_msg[],
-		int lines_to_center, int recommended_width)
+		int lines_to_center, int block_center, int recommended_width)
 {
-	enum { margin = 1 };
+	/* TODO: refactor this function draw_msg() */
 
 	int sw, sh;
-	int w, h;
-	int max_h;
-	int len;
+	int w;
 	size_t ctrl_msg_n;
 	size_t wctrl_msg;
 	int first_line_x = 1;
 	const int first_line_y = 2;
+	int block_margin;
 
 	if(curr_stats.load_stage < 1)
 	{
 		return;
 	}
 
-	curs_set(0);
+	ui_set_cursor(/*visibility=*/0);
 
 	getmaxyx(stdscr, sh, sw);
 
-	ctrl_msg_n = MAX(measure_sub_lines(ctrl_msg, &wctrl_msg), 1U);
+	ctrl_msg_n = MAX(measure_sub_lines(ctrl_msg, /*skip_empty=*/0, &wctrl_msg),
+	                 1U);
 
-	max_h = sh - 2 - ctrl_msg_n - ui_stat_height();
-	h = max_h;
+	int wmsg = measure_text_width(msg);
+
+	/* We start with maximum height and reduce is later. */
+	int max_h = sh - 2 - ui_stat_height();
 	/* The outermost condition is for VLA below (to calm static analyzers). */
-	w = MAX(2 + 2*margin, MIN(sw - 2,
-	        MAX(MAX(recommended_width, sw/3),
-	            (int)MAX(wctrl_msg, determine_width(msg)) + 4)));
-	wresize(error_win, h, w);
+	w = MAX(2 + 2*MARGIN, MIN(sw - 2,
+	        MAX(MAX(recommended_width, sw/3), MAX((int)wctrl_msg, wmsg) + 4)));
+	wresize(error_win, max_h, w);
 
 	werase(error_win);
 
-	len = strlen(msg);
-	if(len <= w - 2 && strchr(msg, '\n') == NULL)
+	block_margin = 0;
+	if(block_center)
 	{
-		first_line_x = (w - len)/2;
-		h = 5 + ctrl_msg_n;
-		wresize(error_win, h, w);
-		mvwin(error_win, (sh - h)/2, (sw - w)/2);
-		checked_wmove(error_win, first_line_y, first_line_x);
-		wprint(error_win, msg);
+		block_margin = MAX(w - wmsg, 2 + 2*MARGIN)/2;
 	}
-	else
-	{
-		int i = 0;
-		int cy = first_line_y;
-		while(i < len)
-		{
-			int j;
-			char buf[w - 2 - 2*margin + 1];
-			int cx;
 
-			copy_str(buf, sizeof(buf), msg + i);
+	if(strchr(msg, '\n') == NULL && wmsg <= w - 2 - 2*MARGIN)
+	{
+		lines_to_center = 1;
+	}
 
-			for(j = 0; buf[j] != '\0'; j++)
-				if(buf[j] == '\n')
-					break;
+	const char *curr = msg;
+	int cy = first_line_y;
+	while(*curr != '\0')
+	{
+		int max_width = w - 2 - 2*MARGIN;
 
-			if(buf[j] != '\0')
-				i++;
-			buf[j] = '\0';
-			i += j;
+		/* Screen line stops at new line or when there is no more space. */
+		const char *nl = until_first(curr, '\n');
+		const char *sl = curr + utf8_strsnlen(curr, max_width);
+		const char *end = (nl < sl ? nl : sl);
 
-			if(buf[0] == '\0')
-				continue;
+		char buf[max_width*10 + 1];
+		copy_str(buf, MIN((int)sizeof(buf), end - curr + 1), curr);
+		curr = (end[0] == '\n' ? end + 1 : end);
+		if(buf[0] == '\0')
+			continue;
 
-			if(cy >= max_h - (int)ctrl_msg_n - 3)
+		if(cy >= max_h - (int)ctrl_msg_n - 3)
+		{
+			/* Skip trailing part of the message if it's too long, just print how
+			 * many lines we're omitting. */
+			size_t max_len;
+			int more_lines = 1U + measure_sub_lines(curr, /*skip_empty=*/1, &max_len);
+			if(more_lines > 1)
 			{
-				/* Skip trailing part of the message if it's too long, just print how
-				 * many lines we're omitting. */
-				size_t max_len;
-				const int more_lines = 1U + measure_sub_lines(msg + i, &max_len);
-				snprintf(buf, sizeof(buf), "<<%d more line%s not shown>>", more_lines,
-						(more_lines == 1) ? "" : "s");
+				snprintf(buf, sizeof(buf), "<<%d more lines not shown>>", more_lines);
 				/* Make sure this is the last iteration of the loop. */
-				i = len;
+				curr += strlen(curr);
 			}
+		}
 
-			h = 1 + cy + 1 + ctrl_msg_n + 1;
-			wresize(error_win, h, w);
-			mvwin(error_win, (sh - h)/2, (sw - w)/2);
-
-			cx = lines_to_center-- > 0 ? (w - utf8_strsw(buf))/2 : (1 + margin);
-			if(cy == first_line_y)
+		int cx = 1 + MARGIN;
+		if(lines_to_center-- > 0)
+		{
+			if(block_center)
+			{
+				cx = block_margin;
+			}
+			else
 			{
-				first_line_x = cx;
+				cx = (w - utf8_strsw(buf))/2;
 			}
-			checked_wmove(error_win, cy++, cx);
-			wprint(error_win, buf);
 		}
+
+		if(cy == first_line_y)
+		{
+			first_line_x = cx;
+		}
+		checked_wmove(error_win, cy++, cx);
+		wprint(error_win, buf);
 	}
 
+	int h = 1 + cy + ctrl_msg_n + 1;
+	wresize(error_win, h, w);
+	mvwin(error_win, (sh - h)/2, (sw - w)/2);
+
 	box(error_win, 0, 0);
 	if(title[0] != '\0')
 	{
 		mvwprintw(error_win, 0, (w - strlen(title) - 2)/2, " %s ", title);
 	}
 
+	int ctrl_msg_width = measure_text_width(ctrl_msg);
+	block_margin = MAX(w - ctrl_msg_width, 2 + 2*MARGIN)/2;
+
 	/* Print control message line by line. */
-	size_t i = ctrl_msg_n;
+	size_t ctrl_i = ctrl_msg_n;
 	while(1)
 	{
 		const size_t len = strcspn(ctrl_msg, "\n");
-		mvwaddnstr(error_win, h - i - 1, MAX(0, (w - (int)len)/2), ctrl_msg, len);
+		mvwaddnstr(error_win, h - ctrl_i - 1, block_margin, ctrl_msg, len);
 
-		if(--i == 0)
+		if(--ctrl_i == 0)
 		{
 			break;
 		}
 
-		ctrl_msg = skip_char(ctrl_msg + len + 1U, '/');
+		ctrl_msg += len + (ctrl_msg[len] == '\n' ? 1 : 0);
 	}
 
 	checked_wmove(error_win, first_line_y, first_line_x);
@@ -626,7 +672,7 @@ draw_msg(const char title[], const char msg[], const char ctrl_msg[],
  * *max_len to the length of the longest sub-line.  Returns total number of
  * sub-lines, which can be zero is msg is an empty line. */
 static size_t
-measure_sub_lines(const char msg[], size_t *max_len)
+measure_sub_lines(const char msg[], int skip_empty, size_t *max_len)
 {
 	size_t nlines = 0U;
 	*max_len = 0U;
@@ -637,38 +683,32 @@ measure_sub_lines(const char msg[], size_t *max_len)
 		{
 			*max_len = len;
 		}
-		++nlines;
+		/* Empty lines are not displayed. */
+		if(len != 0 || !skip_empty)
+		{
+			++nlines;
+		}
 		msg += len + (msg[len] == '\n' ? 1U : 0U);
 	}
 	return nlines;
 }
 
-/* Determines maximum width of line in the message.  Returns the width. */
+/* Measures maximum on-screen width of a line in a message.  Returns the
+ * width. */
 static size_t
-determine_width(const char msg[])
+measure_text_width(const char msg[])
 {
 	size_t max_width = 0U;
-
 	while(*msg != '\0')
 	{
-		size_t width = 0U;
-		while(*msg != '\n' && *msg != '\0')
-		{
-			++width;
-			++msg;
-		}
-
+		const size_t len = strcspn(msg, "\n");
+		const size_t width = utf8_nstrsw(msg, len);
 		if(width > max_width)
 		{
 			max_width = width;
 		}
-
-		if(*msg == '\n')
-		{
-			++msg;
-		}
+		msg += len + (msg[len] == '\n' ? 1U : 0U);
 	}
-
 	return max_width;
 }
 
@@ -710,10 +750,18 @@ confirm_deletion(char *files[], int nfiles, int use_trash)
 		}
 	}
 
-	prompt_msg_internal(title, msg, NULL, 1);
+	dialog_data_t data = {
+		.details = {
+			.title = title,
+			.message = msg,
+		},
+		.kind = D_QUERY_CENTER_FIRST,
+		.accept_mask = MASK(DR_YES, DR_NO),
+	};
+	prompt_msg_internal(&data);
 	free(msg);
 
-	if(dialog_result != DR_YES)
+	if(data.result != DR_YES)
 	{
 		return 0;
 	}
diff --git a/src/modes/dialogs/msg_dialog.h b/src/modes/dialogs/msg_dialog.h
index 90cf1f2..9eb0d20 100644
--- a/src/modes/dialogs/msg_dialog.h
+++ b/src/modes/dialogs/msg_dialog.h
@@ -24,6 +24,29 @@
 
 #include <stdio.h> /* FILE */
 
+/* Input data for prompt_msg_custom() that describes prompt. */
+typedef struct custom_prompt_t
+{
+	const char *title;   /* Dialog's title. */
+	const char *message; /* Dialog's body, can be NULL if make_message isn't. */
+
+	/* If non-NULL, used to generate dialog's body dynamically.  Should return a
+	 * newly allocated string or NULL on error in which case the prompt is
+	 * aborted. */
+	char * (*make_message)(int max_w, int max_h, void *data);
+	/* Extra argument for make_message(). */
+	void *user_data;
+
+	/* Should be terminated with a record filled with zeroes.  The array must
+	 * contain at least one item.  Use '\r' key to handle Enter and '\x03' to
+	 * handle cancellation (both Ctrl-C and Escape).  Elements with empty lines
+	 * instead of descriptions are not displayed. */
+	const struct response_variant *variants;
+	/* Whether message lines should be centered as a block. */
+	int block_center;
+}
+custom_prompt_t;
+
 /* Definition of a dialog option. */
 typedef struct response_variant
 {
@@ -63,14 +86,9 @@ int prompt_msg(const char title[], const char message[]);
 int prompt_msgf(const char title[], const char format[], ...)
 	_gnuc_printf(2, 3);
 
-/* Same as prompt_msg() but with custom list of options.  The responses array
- * should be terminated with a record filled with zeroes.  Returns one of keys
- * defined in the array.  The array has to contain at least one element.  Use
- * '\r' key to handle Enter and '\x03' to handle cancellation (both Ctrl-C and
- * Escape).  variants elements with empty lines instead of descriptions are not
- * displayed. */
-char prompt_msg_custom(const char title[], const char message[],
-		const response_variant variants[]);
+/* Same as prompt_msg() but with custom list of options.  Returns one of the
+ * keys defined in the array of details->variants. */
+char prompt_msg_custom(const custom_prompt_t *details);
 
 /* Draws centered formatted message with specified title and control message on
  * error_win. */
diff --git a/src/modes/dialogs/sort_dialog.c b/src/modes/dialogs/sort_dialog.c
index 832e84e..c994bd9 100644
--- a/src/modes/dialogs/sort_dialog.c
+++ b/src/modes/dialogs/sort_dialog.c
@@ -31,6 +31,7 @@
 #include "../../ui/colors.h"
 #include "../../ui/ui.h"
 #include "../../utils/macros.h"
+#include "../../utils/utils.h"
 #include "../../filelist.h"
 #include "../../status.h"
 #include "../modes.h"
@@ -330,11 +331,8 @@ cmd_h(key_info_t key_info, keys_info_t *keys_info)
 static void
 cmd_j(key_info_t key_info, keys_info_t *keys_info)
 {
-	if(key_info.count == NO_COUNT_GIVEN)
-		key_info.count = 1;
-
 	clear_at_pos();
-	curr += key_info.count;
+	curr += def_count(key_info.count);
 	if(curr > bottom)
 		curr = bottom;
 
@@ -345,11 +343,8 @@ cmd_j(key_info_t key_info, keys_info_t *keys_info)
 static void
 cmd_k(key_info_t key_info, keys_info_t *keys_info)
 {
-	if(key_info.count == NO_COUNT_GIVEN)
-		key_info.count = 1;
-
 	clear_at_pos();
-	curr -= key_info.count;
+	curr -= def_count(key_info.count);
 	if(curr < top)
 		curr = top;
 
diff --git a/src/modes/file_info.c b/src/modes/file_info.c
index a84836d..7a5a9c6 100644
--- a/src/modes/file_info.c
+++ b/src/modes/file_info.c
@@ -332,7 +332,8 @@ show_file_type(view_t *view, draw_ctx_t *ctx)
 		get_current_full_path(view, sizeof(full_path), full_path);
 
 		/* Use the file command to get file information. */
-		escaped_full_path = shell_like_escape(full_path, 0);
+		ShellType shell_type = (get_env_type() == ET_UNIX ? ST_POSIX : ST_CMD);
+		escaped_full_path = shell_arg_escape(full_path, shell_type);
 		snprintf(command, sizeof(command), "file %s -b", escaped_full_path);
 		free(escaped_full_path);
 
diff --git a/src/modes/menu.c b/src/modes/menu.c
index 486a77b..359f37b 100644
--- a/src/modes/menu.c
+++ b/src/modes/menu.c
@@ -108,7 +108,9 @@ static void cmd_zh(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_zl(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_zt(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_zz(key_info_t key_info, keys_info_t *keys_info);
+static void handle_mouse_event(key_info_t key_info, keys_info_t *keys_info);
 static int all_lines_visible(const menu_data_t *menu);
+
 static int goto_cmd(const cmd_info_t *cmd_info);
 static int nohlsearch_cmd(const cmd_info_t *cmd_info);
 static int quit_cmd(const cmd_info_t *cmd_info);
@@ -174,6 +176,7 @@ static keys_add_info_t builtin_cmds[] = {
 	{{WC_z, K(KEY_LEFT)},  {{&cmd_zh},     .descr = "scroll one column left"}},
 	{{WC_z, K(KEY_RIGHT)}, {{&cmd_zl},     .descr = "scroll one column right"}},
 #endif /* ENABLE_EXTENDED_KEYS */
+	{{K(KEY_MOUSE)}, {{&handle_mouse_event}, FOLLOWED_BY_NONE}},
 };
 
 /* Specification of builtin commands. */
@@ -564,7 +567,7 @@ cmd_slash(key_info_t key_info, keys_info_t *keys_info)
 	last_search_backward = 0;
 	menus_search_reset(menu->state, last_search_backward,
 			def_count(key_info.count));
-	modcline_enter(CLS_MENU_FSEARCH, "", menu);
+	modcline_in_menu(CLS_MENU_FSEARCH, menu);
 }
 
 /* Jump to percent of list. */
@@ -587,7 +590,7 @@ cmd_colon(key_info_t key_info, keys_info_t *keys_info)
 	cmds_conf.begin = 1;
 	cmds_conf.current = menu->pos;
 	cmds_conf.end = menu->len;
-	modcline_enter(CLS_MENU_COMMAND, "", menu);
+	modcline_in_menu(CLS_MENU_COMMAND, menu);
 }
 
 static void
@@ -596,7 +599,7 @@ cmd_qmark(key_info_t key_info, keys_info_t *keys_info)
 	last_search_backward = 1;
 	menus_search_reset(menu->state, last_search_backward,
 			def_count(key_info.count));
-	modcline_enter(CLS_MENU_BSEARCH, "", menu);
+	modcline_in_menu(CLS_MENU_BSEARCH, menu);
 }
 
 /* Populates very custom (unsorted) view with list of files. */
@@ -844,8 +847,10 @@ cmd_v(key_info_t key_info, keys_info_t *keys_info)
 
 	if(!qf)
 	{
-		char *const arg = shell_like_escape("+exe 'bd!|args' "
-				"join(map(getline('1','$'),'fnameescape(v:val)'))", 0);
+		ShellType shell_type = (get_env_type() == ET_UNIX ? ST_POSIX : ST_CMD);
+		char *const arg = shell_arg_escape("+exe 'bd!|args' "
+				"join(map(getline('1','$'),'fnameescape(v:val)'))",
+				shell_type);
 		cmd = format_str("%s %s +argument%d -", vi_cmd, arg, menu->pos + 1);
 		free(arg);
 	}
@@ -1051,7 +1056,7 @@ modmenu_morph_into_cline(CmdLineSubmode submode, const char input[],
 	}
 
 	leave_menu_mode(0);
-	modcline_enter(submode, input_copy, NULL);
+	modcline_enter(submode, input_copy);
 
 	free(input_copy);
 }
@@ -1083,7 +1088,7 @@ leave_menu_mode(int reset_selection)
 void
 modmenu_run_command(const char cmd[])
 {
-	if(exec_command(cmd, view, CIT_COMMAND) < 0)
+	if(cmds_dispatch1(cmd, view, CIT_COMMAND) < 0)
 	{
 		ui_sb_err("An error occurred while trying to execute command");
 	}
@@ -1097,5 +1102,44 @@ menu_get_current(void)
 	return menu;
 }
 
+/* Processes events from the mouse. */
+static void
+handle_mouse_event(key_info_t key_info, keys_info_t *keys_info)
+{
+	MEVENT e;
+	if(ui_get_mouse(&e) != OK)
+	{
+		return;
+	}
+
+	if(!wenclose(menu_win, e.y, e.x))
+	{
+		return;
+	}
+
+	if(e.bstate & BUTTON1_PRESSED)
+	{
+		wmouse_trafo(menu_win, &e.y, &e.x, FALSE);
+
+		int old_pos = menu->pos;
+
+		menus_erase_current(menu->state);
+		menus_set_pos(menu->state, menu->top + e.y - 1);
+		ui_refresh_win(menu_win);
+
+		if(menu->pos == old_pos)
+		{
+			cmd_return(key_info, keys_info);
+		}
+	}
+	else if(e.bstate & BUTTON4_PRESSED)
+	{
+		cmd_ctrl_y(key_info, keys_info);
+	}
+	else if(e.bstate & (BUTTON2_PRESSED | BUTTON5_PRESSED))
+	{
+		cmd_ctrl_e(key_info, keys_info);
+	}
+}
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 filetype=c : */
diff --git a/src/modes/modes.c b/src/modes/modes.c
index 68aced2..b2b2fdf 100644
--- a/src/modes/modes.c
+++ b/src/modes/modes.c
@@ -30,6 +30,7 @@
 #include "../ui/ui.h"
 #include "../utils/log.h"
 #include "../utils/macros.h"
+#include "../compare.h"
 #include "../event_loop.h"
 #include "../status.h"
 #include "dialogs/attr_dialog.h"
@@ -47,6 +48,7 @@
 static int mode_flags[] = {
 	MF_USES_COUNT | MF_USES_REGS, /* NORMAL_MODE */
 	MF_USES_INPUT,                /* CMDLINE_MODE */
+	MF_USES_INPUT,                /* NAV_MODE */
 	MF_USES_COUNT | MF_USES_REGS, /* VISUAL_MODE */
 	MF_USES_COUNT,                /* MENU_MODE */
 	MF_USES_COUNT,                /* SORT_MODE */
@@ -62,6 +64,7 @@ ARRAY_GUARD(mode_flags, MODES_COUNT);
 static char uses_input_bar[] = {
 	1, /* NORMAL_MODE */
 	0, /* CMDLINE_MODE */
+	0, /* NAV_MODE */
 	1, /* VISUAL_MODE */
 	1, /* MENU_MODE */
 	1, /* SORT_MODE */
@@ -78,6 +81,7 @@ typedef void (*mode_init_func)(void);
 static mode_init_func mode_init_funcs[] = {
 	&modnorm_init,            /* NORMAL_MODE */
 	&modcline_init,           /* CMDLINE_MODE */
+	&modnav_init,             /* NAV_MODE */
 	&modvis_init,             /* VISUAL_MODE */
 	&modmenu_init,            /* MENU_MODE */
 	&init_sort_dialog_mode,   /* SORT_MODE */
@@ -90,15 +94,16 @@ static mode_init_func mode_init_funcs[] = {
 };
 ARRAY_GUARD(mode_init_funcs, MODES_COUNT);
 
-static void modes_statusbar_update(void);
+static void print_compare_stats(void);
 static void update_vmode_input(void);
 
 void
-init_modes(void)
+modes_init(void)
 {
 	LOG_FUNC_ENTER;
 
 	vle_keys_init(MODES_COUNT, mode_flags, &stats_silence_ui);
+	vle_mode_set(NORMAL_MODE, VMT_PRIMARY);
 
 	int i;
 	for(i = 0; i < MODES_COUNT; ++i)
@@ -114,7 +119,7 @@ modes_pre(void)
 	{
 		/* Do nothing for these modes. */
 	}
-	else if(vle_mode_is(CMDLINE_MODE))
+	else if(modes_is_cmdline_like())
 	{
 		touchwin(status_bar);
 		ui_refresh_win(status_bar);
@@ -123,7 +128,7 @@ modes_pre(void)
 	{
 		modview_pre();
 	}
-	else if(is_in_menu_like_mode())
+	else if(modes_is_menu_like())
 	{
 		modmenu_pre();
 	}
@@ -145,7 +150,7 @@ void
 modes_post(void)
 {
 	if(ANY(vle_mode_is,
-				CMDLINE_MODE, SORT_MODE, CHANGE_MODE, ATTR_MODE, MORE_MODE))
+				CMDLINE_MODE, NAV_MODE, SORT_MODE, CHANGE_MODE, ATTR_MODE, MORE_MODE))
 	{
 		/* Do nothing for these modes. */
 		return;
@@ -155,7 +160,7 @@ modes_post(void)
 		modview_post();
 		return;
 	}
-	else if(is_in_menu_like_mode())
+	else if(modes_is_menu_like())
 	{
 		modmenu_post();
 		return;
@@ -183,16 +188,36 @@ modes_post(void)
 void
 modes_statusbar_update(void)
 {
-	if(vle_mode_is(MORE_MODE))
+	if(ANY(vle_mode_is, MORE_MODE, CMDLINE_MODE, NAV_MODE))
 	{
-		/* Status bar is used for special purposes. */
+		return;
 	}
-	else if(!curr_stats.save_msg &&
-			(curr_view->selected_files || vle_mode_is(VISUAL_MODE)))
+	else if(curr_stats.save_msg != 0)
 	{
-		print_selected_msg();
+		if(vle_mode_is(VISUAL_MODE))
+		{
+			update_vmode_input();
+		}
+		return;
 	}
-	else if(!curr_stats.save_msg)
+
+	if(vle_mode_is(VISUAL_MODE))
+	{
+		ui_sb_msgf("-- %s -- ", modvis_describe());
+		update_vmode_input();
+		curr_stats.save_msg = 2;
+	}
+	else if(curr_view->selected_files)
+	{
+		ui_sb_msgf("%d %s selected", curr_view->selected_files,
+				curr_view->selected_files == 1 ? "file" : "files");
+		curr_stats.save_msg = 2;
+	}
+	else if(cv_compare(curr_view->custom.type))
+	{
+		print_compare_stats();
+	}
+	else
 	{
 		ui_sb_clear();
 	}
@@ -223,7 +248,7 @@ modes_redraw(void)
 		goto finish;
 	}
 
-	if(vle_mode_is(CMDLINE_MODE))
+	if(modes_is_cmdline_like())
 	{
 		modcline_redraw();
 		goto finish;
@@ -294,7 +319,7 @@ finish:
 void
 modes_update(void)
 {
-	if(vle_mode_is(CMDLINE_MODE))
+	if(modes_is_cmdline_like())
 	{
 		modcline_redraw();
 		return;
@@ -328,11 +353,11 @@ modes_update(void)
 }
 
 void
-modupd_input_bar(wchar_t *str)
+modes_input_bar_update(const wchar_t str[])
 {
 	if(vle_mode_is(VISUAL_MODE))
 	{
-		clear_input_bar();
+		modes_input_bar_clear();
 	}
 
 	if(uses_input_bar[vle_mode_get()])
@@ -342,7 +367,7 @@ modupd_input_bar(wchar_t *str)
 }
 
 void
-clear_input_bar(void)
+modes_input_bar_clear(void)
 {
 	if(uses_input_bar[vle_mode_get()] && !vle_mode_is(VISUAL_MODE))
 	{
@@ -351,7 +376,7 @@ clear_input_bar(void)
 }
 
 int
-is_in_menu_like_mode(void)
+modes_is_menu_like(void)
 {
 	return ANY(vle_primary_mode_is, MENU_MODE, FILE_INFO_MODE, MORE_MODE);
 }
@@ -362,8 +387,14 @@ modes_is_dialog_like(void)
 	return ANY(vle_mode_is, SORT_MODE, ATTR_MODE, CHANGE_MODE, MSG_MODE);
 }
 
+int
+modes_is_cmdline_like(void)
+{
+	return ANY(vle_mode_is, CMDLINE_MODE, NAV_MODE);
+}
+
 void
-abort_menu_like_mode(void)
+modes_abort_menu_like(void)
 {
 	if(vle_primary_mode_is(MENU_MODE))
 	{
@@ -379,19 +410,42 @@ abort_menu_like_mode(void)
 	}
 }
 
-void
-print_selected_msg(void)
+/* Prints compare stats on status line. */
+static void
+print_compare_stats(void)
 {
-	if(vle_mode_is(VISUAL_MODE))
+	int flags = curr_view->custom.diff_cmp_flags;
+	const compare_stats_t *stats = &curr_view->custom.diff_stats;
+
+	if(flags & CF_SINGLE_PANE)
 	{
-		ui_sb_msgf("-- %s -- ", modvis_describe());
-		update_vmode_input();
+		return;
+	}
+
+	if(flags & CF_GROUP_PATHS)
+	{
+		ui_sb_msgf("(on compare) "
+				"%cidentical: %d, %cdifferent: %d, %c/%cunique: %d/%d",
+				flags & CF_SHOW_IDENTICAL ? '+' : '-',
+				stats->identical,
+				flags & CF_SHOW_DIFFERENT ? '+' : '-',
+				stats->different,
+				flags & CF_SHOW_UNIQUE_LEFT ? '+' : '-',
+				flags & CF_SHOW_UNIQUE_RIGHT ? '+' : '-',
+				stats->unique_left,
+				stats->unique_right);
 	}
 	else
 	{
-		ui_sb_msgf("%d %s selected", curr_view->selected_files,
-				curr_view->selected_files == 1 ? "file" : "files");
+		ui_sb_msgf("(on compare) %cidentical: %d, %c/%cunique: %d/%d",
+				flags & CF_SHOW_IDENTICAL ? '+' : '-',
+				stats->identical,
+				flags & CF_SHOW_UNIQUE_LEFT ? '+' : '-',
+				flags & CF_SHOW_UNIQUE_RIGHT ? '+' : '-',
+				stats->unique_left,
+				stats->unique_right);
 	}
+
 	curr_stats.save_msg = 2;
 }
 
diff --git a/src/modes/modes.h b/src/modes/modes.h
index 8705e29..fbd6093 100644
--- a/src/modes/modes.h
+++ b/src/modes/modes.h
@@ -26,6 +26,7 @@ enum
 {
 	NORMAL_MODE,
 	CMDLINE_MODE,
+	NAV_MODE,
 	VISUAL_MODE,
 	MENU_MODE,
 	SORT_MODE,
@@ -38,33 +39,47 @@ enum
 	MODES_COUNT
 };
 
-void init_modes(void);
+/* Registers modes. */
+void modes_init(void);
 
+/* A hook-like function that performs mode-specific actions at the start of an
+ * event loop iteration. */
 void modes_pre(void);
 
 /* Executes poll-based requests for any of the active modes. */
 void modes_periodic(void);
 
+/* A hook-like function that performs mode-specific actions at the end of an
+ * event loop iteration. */
 void modes_post(void);
 
+/* Redraws currently active mode. */
 void modes_redraw(void);
 
+/* A lighter version of modes_redraw(). */
 void modes_update(void);
 
-void modupd_input_bar(wchar_t *str);
+/* Clears input bar or draws current input there depending on the mode. */
+void modes_input_bar_update(const wchar_t str[]);
 
-void clear_input_bar(void);
+/* Clears input bar if it's being used by the current mode. */
+void modes_input_bar_clear(void);
 
 /* Returns non-zero if current mode is a menu like one. */
-int is_in_menu_like_mode(void);
+int modes_is_menu_like(void);
 
 /* Checks if current mode is a dialog.  Returns non-zero if so. */
 int modes_is_dialog_like(void);
 
+/* Returns non-zero if current mode is a command-line like one. */
+int modes_is_cmdline_like(void);
+
 /* Aborts one of menu-like modes if any of them is currently active. */
-void abort_menu_like_mode(void);
+void modes_abort_menu_like(void);
 
-void print_selected_msg(void);
+/* Either prints appropriate message on the statusbar or clears it depending on
+ * the mode and its state. */
+void modes_statusbar_update(void);
 
 #endif /* VIFM__MODES__MODES_H__ */
 
diff --git a/src/modes/more.c b/src/modes/more.c
index 6d7cb8c..c496908 100644
--- a/src/modes/more.c
+++ b/src/modes/more.c
@@ -269,7 +269,7 @@ static void
 cmd_colon(key_info_t key_info, keys_info_t *keys_info)
 {
 	leave_more_mode();
-	modcline_enter(CLS_COMMAND, "", NULL);
+	modcline_enter(CLS_COMMAND, "");
 }
 
 /* Navigate to the bottom. */
diff --git a/src/modes/normal.c b/src/modes/normal.c
index 6ab0533..afbb983 100644
--- a/src/modes/normal.c
+++ b/src/modes/normal.c
@@ -41,6 +41,7 @@
 #include "../engine/keys.h"
 #include "../engine/mode.h"
 #include "../engine/variables.h"
+#include "../menus/filetypes_menu.h"
 #include "../modes/dialogs/msg_dialog.h"
 #include "../ui/cancellation.h"
 #include "../ui/fileview.h"
@@ -87,7 +88,6 @@ static void cmd_ctrl_c(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_ctrl_d(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_ctrl_e(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_ctrl_f(key_info_t key_info, keys_info_t *keys_info);
-static void page_scroll(int base, int direction);
 static void cmd_ctrl_g(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_space(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_emarkemark(key_info_t key_info, keys_info_t *keys_info);
@@ -247,6 +247,8 @@ static void pick_files(view_t *view, int end, keys_info_t *keys_info);
 static void selector_S(key_info_t key_info, keys_info_t *keys_info);
 static void selector_a(key_info_t key_info, keys_info_t *keys_info);
 static void selector_s(key_info_t key_info, keys_info_t *keys_info);
+static void set_pos_in_curr_view(int pos);
+static void handle_mouse_event(key_info_t key_info, keys_info_t *keys_info);
 
 static int last_fast_search_char;
 static int last_fast_search_backward = -1;
@@ -345,8 +347,8 @@ static keys_add_info_t builtin_cmds[] = {
 	{WK_c WK_w,        {{&cmd_cw}, .descr = "rename files"}},
 	{WK_D WK_D,        {{&cmd_DD}, .nim = 1, .descr = "remove files permanently"}},
 	{WK_d WK_d,        {{&cmd_dd}, .nim = 1, .descr = "remove files"}},
-	{WK_d WK_o,        {{&cmd_do}, .descr = "obtain current file"}},
-	{WK_d WK_p,        {{&cmd_dp}, .descr = "put current file"}},
+	{WK_d WK_o,        {{&cmd_do}, .descr = "obtain files"}},
+	{WK_d WK_p,        {{&cmd_dp}, .descr = "put files"}},
 	{WK_D,             {{&cmd_D_selector}, FOLLOWED_BY_SELECTOR, .descr = "remove files permanently"}},
 	{WK_d,             {{&cmd_d_selector}, FOLLOWED_BY_SELECTOR, .descr = "remove files"}},
 	{WK_e,             {{&cmd_e}, .descr = "explore file contents"}},
@@ -441,6 +443,7 @@ static keys_add_info_t builtin_cmds[] = {
 #else
 	{WK_ESC L"[Z",               {{&cmd_shift_tab}, .descr = "switch to view pane"}},
 #endif /* ENABLE_EXTENDED_KEYS */
+	{{K(KEY_MOUSE)}, {{&handle_mouse_event}, FOLLOWED_BY_NONE}},
 };
 
 static keys_add_info_t selectors[] = {
@@ -516,10 +519,7 @@ cmd_ctrl_a(key_info_t key_info, keys_info_t *keys_info)
 static void
 cmd_ctrl_b(key_info_t key_info, keys_info_t *keys_info)
 {
-	if(can_scroll_up(curr_view))
-	{
-		page_scroll(fpos_get_last_visible_cell(curr_view), -1);
-	}
+	fview_scroll_page_up(curr_view);
 }
 
 /* Resets selection and search highlight. */
@@ -546,9 +546,9 @@ cmd_ctrl_d(key_info_t key_info, keys_info_t *keys_info)
 static void
 cmd_ctrl_e(key_info_t key_info, keys_info_t *keys_info)
 {
-	if(fpos_has_hidden_bottom(curr_view) && fpos_scroll_down(curr_view, 1))
+	if(fpos_can_scroll_fwd(curr_view) && fpos_scroll_down(curr_view, 1))
 	{
-		scroll_down(curr_view, 1);
+		fview_scroll_fwd_by(curr_view, 1);
 		redraw_current_view();
 	}
 }
@@ -556,31 +556,7 @@ cmd_ctrl_e(key_info_t key_info, keys_info_t *keys_info)
 static void
 cmd_ctrl_f(key_info_t key_info, keys_info_t *keys_info)
 {
-	if(can_scroll_down(curr_view))
-	{
-		page_scroll(curr_view->top_line, 1);
-	}
-}
-
-/* Scrolls pane by one view in both directions.  The direction should be 1 or
- * -1. */
-static void
-page_scroll(int base, int direction)
-{
-	enum { HOR_GAP_SIZE = 2, VER_GAP_SIZE = 1 };
-	int old_pos = curr_view->list_pos;
-	int offset = fview_is_transposed(curr_view)
-	    ? (MAX(1, curr_view->column_count - VER_GAP_SIZE))*curr_view->window_rows
-	    : (curr_view->window_rows - HOR_GAP_SIZE)*curr_view->column_count;
-	int new_pos = base + direction*offset
-	            + old_pos%curr_view->run_size - base%curr_view->run_size;
-	curr_view->list_pos = MAX(0, MIN(curr_view->list_rows - 1, new_pos));
-	scroll_by_files(curr_view, direction*offset);
-
-	/* Updating list_pos ourselves doesn't take into account
-	 * synchronization/updates of the other view, so trigger them. */
-	ui_view_schedule_redraw(curr_view);
-	fpos_set_pos(curr_view, curr_view->list_pos);
+	fview_scroll_page_down(curr_view);
 }
 
 static void
@@ -618,7 +594,7 @@ cmd_emarkemark(key_info_t key_info, keys_info_t *keys_info)
 	}
 
 	cmds_vars_set_count(key_info.count);
-	modcline_enter(CLS_COMMAND, prefix, NULL);
+	modcline_enter(CLS_COMMAND, prefix);
 }
 
 /* Processes !<selector> normal mode command.  Processes results of applying
@@ -930,9 +906,9 @@ cmd_ctrl_x(key_info_t key_info, keys_info_t *keys_info)
 static void
 cmd_ctrl_y(key_info_t key_info, keys_info_t *keys_info)
 {
-	if(fpos_has_hidden_top(curr_view) && fpos_scroll_up(curr_view, 1))
+	if(fpos_can_scroll_back(curr_view) && fpos_scroll_up(curr_view, 1))
 	{
-		scroll_up(curr_view, 1);
+		fview_scroll_back_by(curr_view, 1);
 		redraw_current_view();
 	}
 }
@@ -1092,8 +1068,6 @@ cmd_gr(key_info_t key_info, keys_info_t *keys_info)
 static void
 cmd_gs(key_info_t key_info, keys_info_t *keys_info)
 {
-	reg_t *reg;
-
 	if(key_info.reg == NO_REG_GIVEN)
 	{
 		flist_sel_restore(curr_view, NULL);
@@ -1101,7 +1075,7 @@ cmd_gs(key_info_t key_info, keys_info_t *keys_info)
 	}
 
 	regs_sync_from_shared_memory();
-	reg = regs_find(tolower(key_info.reg));
+	const reg_t *reg = regs_find(tolower(key_info.reg));
 	if(reg == NULL || reg->nfiles < 1)
 	{
 		ui_sb_err(reg == NULL ? "No such register" : "Register is empty");
@@ -1329,7 +1303,7 @@ cmd_percent(key_info_t key_info, keys_info_t *keys_info)
 static void
 cmd_equal(key_info_t key_info, keys_info_t *keys_info)
 {
-	modcline_enter(CLS_FILTER, curr_view->local_filter.filter.raw, NULL);
+	modcline_enter(CLS_FILTER, curr_view->local_filter.filter.raw);
 }
 
 /* Continues navigation to word which starts with specified character in
@@ -1348,7 +1322,7 @@ cmd_comma(key_info_t key_info, keys_info_t *keys_info)
 static void
 cmd_dot(key_info_t key_info, keys_info_t *keys_info)
 {
-	curr_stats.save_msg = exec_commands(curr_stats.last_cmdline_command,
+	curr_stats.save_msg = cmds_dispatch(curr_stats.last_cmdline_command,
 			curr_view, CIT_COMMAND);
 }
 
@@ -1374,7 +1348,7 @@ cmd_colon(key_info_t key_info, keys_info_t *keys_info)
 	}
 
 	cmds_vars_set_count(key_info.count);
-	modcline_enter(CLS_COMMAND, prefix, NULL);
+	modcline_enter(CLS_COMMAND, prefix);
 }
 
 /* Continues navigation to word which starts with specified character in initial
@@ -1439,9 +1413,12 @@ cmd_cl(key_info_t key_info, keys_info_t *keys_info)
 {
 	if(curr_view->selected_files > 1)
 	{
-		flist_set_marking(curr_view, 0);
+		flist_set_marking(curr_view, /*prefer_current=*/0);
+	}
+	else
+	{
+		clear_marking(curr_view);
 	}
-
 	curr_stats.save_msg = fops_retarget(curr_view);
 }
 
@@ -1715,42 +1692,9 @@ cmd_n(key_info_t key_info, keys_info_t *keys_info)
 static void
 search(key_info_t key_info, int backward)
 {
-	/* TODO: extract common part of this function and visual.c:search(). */
-
-	int found;
-
-	if(hist_is_empty(&curr_stats.search_hist))
-	{
-		return;
-	}
-
-	if(key_info.count == NO_COUNT_GIVEN)
-		key_info.count = 1;
-
-	found = 0;
-	if(curr_view->matches == 0)
-	{
-		const char *const pattern = hists_search_last();
-		curr_stats.save_msg = (find_pattern(curr_view, pattern, backward, 1, &found,
-				0) != 0);
-		--key_info.count;
-	}
-
-	while(key_info.count-- > 0)
-	{
-		found += goto_search_match(curr_view, backward) != 0;
-	}
-
-	if(found)
-	{
-		print_search_next_msg(curr_view, backward);
-	}
-	else
-	{
-		print_search_fail_msg(curr_view, backward);
-	}
-
-	curr_stats.save_msg = 1;
+	curr_stats.save_msg = search_next(curr_view, backward,
+			/*stash_selection=*/cfg.hl_search, /*select_matches=*/cfg.hl_search,
+			def_count(key_info.count), &set_pos_in_curr_view);
 }
 
 /* Put files. */
@@ -1788,7 +1732,7 @@ call_put_links(key_info_t key_info, int relative)
 static void
 cmd_q_colon(key_info_t key_info, keys_info_t *keys_info)
 {
-	get_and_execute_command("", 0U, CIT_COMMAND);
+	cmds_run_ext("", 0U, CIT_COMMAND);
 }
 
 /* Runs external editor to get search pattern and then executes it. */
@@ -1811,17 +1755,17 @@ activate_search(int count, int back, int external)
 {
 	/* TODO: generalize with visual.c:activate_search(). */
 
-	search_repeat = (count == NO_COUNT_GIVEN) ? 1 : count;
+	search_repeat = def_count(count);
 	curr_stats.last_search_backward = back;
 	if(external)
 	{
 		CmdInputType type = back ? CIT_BSEARCH_PATTERN : CIT_FSEARCH_PATTERN;
-		get_and_execute_command("", 0U, type);
+		cmds_run_ext("", 0U, type);
 	}
 	else
 	{
 		const CmdLineSubmode submode = back ? CLS_BSEARCH : CLS_FSEARCH;
-		modcline_enter(submode, "", NULL);
+		modcline_enter(submode, "");
 	}
 }
 
@@ -1829,7 +1773,7 @@ activate_search(int count, int back, int external)
 static void
 cmd_q_equals(key_info_t key_info, keys_info_t *keys_info)
 {
-	get_and_execute_command("", 0U, CIT_FILTER_PATTERN);
+	cmds_run_ext("", 0U, CIT_FILTER_PATTERN);
 }
 
 /* Toggles selection of the current file. */
@@ -2013,10 +1957,10 @@ cmd_zd(key_info_t key_info, keys_info_t *keys_info)
 void
 modnorm_zb(key_info_t key_info, keys_info_t *keys_info)
 {
-	if(can_scroll_up(curr_view))
+	if(fpos_can_scroll_back(curr_view))
 	{
 		const int bottom = fpos_get_bottom_pos(curr_view);
-		scroll_up(curr_view, bottom - curr_view->list_pos);
+		fview_scroll_back_by(curr_view, bottom - curr_view->list_pos);
 		redraw_current_view();
 	}
 }
@@ -2207,10 +2151,10 @@ cmd_right_curly_bracket(key_info_t key_info, keys_info_t *keys_info)
 void
 modnorm_zt(key_info_t key_info, keys_info_t *keys_info)
 {
-	if(can_scroll_down(curr_view))
+	if(fpos_can_scroll_fwd(curr_view))
 	{
 		const int top = fpos_get_top_pos(curr_view);
-		scroll_down(curr_view, curr_view->list_pos - top);
+		fview_scroll_fwd_by(curr_view, curr_view->list_pos - top);
 		redraw_current_view();
 	}
 }
@@ -2221,7 +2165,7 @@ modnorm_zz(key_info_t key_info, keys_info_t *keys_info)
 	if(!fpos_are_all_files_visible(curr_view))
 	{
 		const int middle = fpos_get_middle_pos(curr_view);
-		scroll_by_files(curr_view, curr_view->list_pos - middle);
+		fview_scroll_by(curr_view, curr_view->list_pos - middle);
 		redraw_current_view();
 	}
 }
@@ -2338,30 +2282,133 @@ selector_s(key_info_t key_info, keys_info_t *keys_info)
 }
 
 int
-modnorm_find(view_t *view, const char pattern[], int backward, int print_errors)
+modnorm_find(view_t *view, const char pattern[], int backward, int print_msg,
+		int *found)
 {
-	const int nrepeats = search_repeat - 1;
-	int i;
-	int save_msg;
-	int found;
+	return search_find(view, pattern, backward, /*stash_selection=*/cfg.hl_search,
+			/*select_matches=*/cfg.hl_search, search_repeat, &set_pos_in_curr_view,
+			print_msg, found);
+}
 
-	/* Reset number of repeats so that future calls are not affected by the
-	 * previous ones. */
-	search_repeat = 1;
+/* Moves cursor to pos, redraws cursor and schedules redraw of curr_view. */
+static void
+set_pos_in_curr_view(int pos)
+{
+	fpos_set_pos(curr_view, pos);
+}
 
-	save_msg = find_pattern(view, pattern, backward, 1, &found, print_errors);
-	if(!print_errors && save_msg < 0)
+void
+modnorm_set_search_attrs(int count, int last_search_backward)
+{
+	search_repeat = count;
+	curr_stats.last_search_backward = last_search_backward;
+}
+
+/* Processes events from the mouse. */
+static void
+handle_mouse_event(key_info_t key_info, keys_info_t *keys_info)
+{
+	MEVENT e;
+	if(ui_get_mouse(&e) != OK)
 	{
-		/* If we're not printing messages, we might be interested in broken
-		 * pattern. */
-		return -1;
+		return;
 	}
 
-	for(i = 0; i < nrepeats; ++i)
+	int on_tab_line = !cfg.pane_tabs && wenclose(tab_line, e.y, e.x);
+
+	if(ui_wenclose(&lwin, lwin.win, e.x, e.y) ||
+			ui_wenclose(&lwin, lwin.title, e.x, e.y))
+	{
+		if(curr_view != &lwin)
+		{
+			go_to_other_window();
+			return;
+		}
+	}
+	else if(ui_wenclose(&rwin, rwin.win, e.x, e.y) ||
+			ui_wenclose(&rwin, rwin.title, e.x, e.y))
+	{
+		if(curr_view != &rwin)
+		{
+			go_to_other_window();
+			return;
+		}
+	}
+	else if(!on_tab_line)
+	{
+		return;
+	}
+
+	if(on_tab_line || ui_wenclose(curr_view, curr_view->title, e.y, e.x))
+	{
+		if(!on_tab_line && !cfg.pane_tabs)
+		{
+			/* Do nothing. */
+		}
+		else if(e.bstate & BUTTON1_PRESSED)
+		{
+			wmouse_trafo(ui_get_tab_line_win(curr_view), &e.y, &e.x, FALSE);
+
+			int tab_num = ui_map_tab_line(curr_view, e.x);
+			if(tab_num >= 0)
+			{
+				tabs_goto(tab_num);
+			}
+		}
+		else if(e.bstate & BUTTON4_PRESSED)
+		{
+			tabs_previous(1);
+		}
+		else if(e.bstate & (BUTTON2_PRESSED | BUTTON5_PRESSED))
+		{
+			tabs_next(1);
+		}
+	}
+	else if(e.bstate & BUTTON1_PRESSED)
+	{
+		wmouse_trafo(curr_view->win, &e.y, &e.x, FALSE);
+
+		/* Only handle clicks on non-blank lines. */
+		int list_pos = fview_map_coordinates(curr_view, e.x, e.y);
+		if(list_pos >= 0)
+		{
+			int old_pos = curr_view->list_pos;
+			fpos_set_pos(curr_view, list_pos);
+
+			if(curr_view->list_pos == old_pos)
+			{
+				cmd_return(key_info, keys_info);
+			}
+		}
+		else if(list_pos == FVM_LEAVE)
+		{
+			cmd_gh(key_info, keys_info);
+		}
+		else if(list_pos == FVM_OPEN)
+		{
+			cmd_i(key_info, keys_info);
+		}
+	}
+	else if(e.bstate & BUTTON3_PRESSED)
+	{
+		wmouse_trafo(curr_view->win, &e.y, &e.x, FALSE);
+
+		/* Only handle clicks on non-blank lines. */
+		int list_pos = fview_map_coordinates(curr_view, e.x, e.y);
+		if(list_pos >= 0)
+		{
+			fpos_set_pos(curr_view, list_pos);
+			curr_stats.save_msg = show_file_menu(curr_view, 0);
+		}
+	}
+	else if(e.bstate & BUTTON4_PRESSED)
+	{
+		cmd_ctrl_y(key_info, keys_info);
+	}
+	else if(e.bstate & (BUTTON2_PRESSED | BUTTON5_PRESSED))
 	{
-		save_msg += (goto_search_match(view, backward) != 0);
+		cmd_ctrl_e(key_info, keys_info);
 	}
-	return save_msg;
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/src/modes/normal.h b/src/modes/normal.h
index 14a9c61..1bc49e4 100644
--- a/src/modes/normal.h
+++ b/src/modes/normal.h
@@ -53,11 +53,14 @@ void modnorm_ctrl_wminus(key_info_t key_info, keys_info_t *keys_info);
 void modnorm_ctrl_wpipe(key_info_t key_info, keys_info_t *keys_info);
 
 /* Kind of callback to allow starting searches from the module and rely on other
- * modules.  Returns new value for status bar message flag, but when
- * print_errors isn't requested can return -1 to indicate issues with the
- * pattern. */
+ * modules.  Returns new value for status bar message flag, but when print_msg
+ * isn't requested can return -1 to indicate issues with the pattern. */
 int modnorm_find(struct view_t *view, const char pattern[], int backward,
-		int print_errors);
+		int print_msg, int *found);
+
+/* Sets search attributes necessary for a modnorm_find() call without prior
+ * normal.c:activate_search() call. */
+void modnorm_set_search_attrs(int count, int last_search_backward);
 
 #endif /* VIFM__MODES__NORMAL_H__ */
 
diff --git a/src/modes/view.c b/src/modes/view.c
index 54148dd..b0eedeb 100644
--- a/src/modes/view.c
+++ b/src/modes/view.c
@@ -202,6 +202,7 @@ static int scroll_to_bottom(modview_info_t *vi);
 static void reload_view(modview_info_t *vi, int silent);
 static void cleanup(modview_info_t *vi);
 static modview_info_t * view_info_alloc(void);
+static void handle_mouse_event(key_info_t key_info, keys_info_t *keys_info);
 TSTATIC int modview_is_raw(modview_info_t *vi);
 TSTATIC int modview_is_detached(modview_info_t *vi);
 TSTATIC const char * modview_current_viewer(modview_info_t *vi);
@@ -298,6 +299,7 @@ static keys_add_info_t builtin_cmds[] = {
 	{WK_y,             {{&cmd_k},       .descr = "scroll one line up"}},
 	{WK_w,             {{&cmd_w},       .descr = "scroll backward one window"}},
 	{WK_z,             {{&cmd_z},       .descr = "scroll forward one window"}},
+	{{K(KEY_MOUSE)},   {{&handle_mouse_event}, FOLLOWED_BY_NONE}},
 #ifndef __PDCURSES__
 	{WK_ALT WK_v,      {{&cmd_b}, .descr = "scroll page up"}},
 #else
@@ -350,9 +352,20 @@ modview_enter(view_t *view, int explore)
 
 	vi->filename = is_dir(full_path) ? format_str("%s/", full_path)
 	                                 : strdup(full_path);
-	vi->viewers = ft_get_viewers(vi->filename);
 	vi->view = view;
 
+	if(is_null_or_empty(curr_view->preview_prg))
+	{
+		vi->viewers = ft_get_viewers(vi->filename);
+	}
+	else
+	{
+		strlist_t previewer = {};
+		previewer.nitems = add_to_string_array(&previewer.items, previewer.nitems,
+				curr_view->preview_prg);
+		vi->viewers = previewer;
+	}
+
 	if(load_view_data(vi, "File exploring", full_path, NOSILENT) != 0)
 	{
 		reset_view_info(vi);
@@ -993,14 +1006,14 @@ static void
 cmd_slash(key_info_t key_info, keys_info_t *keys_info)
 {
 	vi->search_repeat = key_info.count;
-	modcline_enter(CLS_VWFSEARCH, "", NULL);
+	modcline_enter(CLS_VWFSEARCH, "");
 }
 
 static void
 cmd_qmark(key_info_t key_info, keys_info_t *keys_info)
 {
 	vi->search_repeat = key_info.count;
-	modcline_enter(CLS_VWBSEARCH, "", NULL);
+	modcline_enter(CLS_VWBSEARCH, "");
 }
 
 /* Switches to the previous viewer. */
@@ -1346,11 +1359,8 @@ cmd_k(key_info_t key_info, keys_info_t *keys_info)
 	if(vi->linev == 0)
 		return;
 
-	if(key_info.count == NO_COUNT_GIVEN)
-		key_info.count = 1;
-	key_info.count = MIN(key_info.count, vi->linev);
-
-	while(key_info.count-- > 0)
+	int repeat_count = MIN(def_count(key_info.count), vi->linev);
+	while(repeat_count-- > 0)
 	{
 		if(vi->linev - 1 < vi->widths[vi->line][0])
 			--vi->line;
@@ -1489,11 +1499,13 @@ find_next(void)
 			vi->line = l;
 			break;
 		}
-		if(l < vi->nlines && (l == vi->nlines - 1 ||
-				vl + 1 >= vi->widths[l + 1][0]))
+		if(l == vi->nlines - 1)
+		{
+			break;
+		}
+
+		if(vl + 1 >= vi->widths[l + 1][0])
 		{
-			if(l == vi->nlines - 1)
-				break;
 			++l;
 			offset = 0;
 		}
@@ -1807,6 +1819,51 @@ modview_detached_get_viewer(void)
 	return (vi == NULL ? NULL : vi->ext_viewer);
 }
 
+/* Processes events from the mouse. */
+static void
+handle_mouse_event(key_info_t key_info, keys_info_t *keys_info)
+{
+	MEVENT e;
+	if(ui_get_mouse(&e) != OK)
+	{
+		return;
+	}
+
+	view_t *selected_view;
+	if(ui_wenclose(&lwin, lwin.win, e.x, e.y) ||
+			ui_wenclose(&lwin, lwin.title, e.x, e.y))
+	{
+		selected_view = &lwin;
+	}
+	else if(ui_wenclose(&rwin, rwin.win, e.x, e.y) ||
+			ui_wenclose(&rwin, rwin.title, e.x, e.y))
+	{
+		selected_view = &rwin;
+	}
+	else
+	{
+		return;
+	}
+
+	if(selected_view != vi->view)
+	{
+		if(!vi->view->explore_mode && (e.bstate & BUTTON1_PRESSED))
+		{
+			cmd_ctrl_ww(key_info, keys_info);
+		}
+		return;
+	}
+
+	if(e.bstate & BUTTON4_PRESSED)
+	{
+		cmd_k(key_info, keys_info);
+	}
+	else if(e.bstate & (BUTTON2_PRESSED | BUTTON5_PRESSED))
+	{
+		cmd_j(key_info, keys_info);
+	}
+}
+
 /* Allocates and initializes view mode information.  Returns pointer to it. */
 static modview_info_t *
 view_info_alloc(void)
diff --git a/src/modes/visual.c b/src/modes/visual.c
index ae52316..020addb 100644
--- a/src/modes/visual.c
+++ b/src/modes/visual.c
@@ -29,6 +29,8 @@
 #include "../compat/curses.h"
 #include "../engine/keys.h"
 #include "../engine/mode.h"
+#include "../menus/filetypes_menu.h"
+#include "../menus/menus.h"
 #include "../modes/dialogs/msg_dialog.h"
 #include "../ui/fileview.h"
 #include "../ui/statusbar.h"
@@ -134,7 +136,7 @@ static void cmd_q_slash(key_info_t key_info, keys_info_t *keys_info);
 static void cmd_q_question(key_info_t key_info, keys_info_t *keys_info);
 static void activate_search(int count, int back, int external);
 static void cmd_n(key_info_t key_info, keys_info_t *keys_info);
-static void search(key_info_t key_info, int backward, int interactive);
+static void search(key_info_t key_info, int backward);
 static void cmd_v(key_info_t key_info, keys_info_t *keys_info);
 static void change_amend_type(AmendType new_amend_type);
 static void cmd_y(key_info_t key_info, keys_info_t *keys_info);
@@ -164,11 +166,12 @@ static void select_up_one(view_t *view, int start_pos);
 static void select_down_one(view_t *view, int start_pos);
 static void apply_selection(int pos);
 static void revert_selection(int pos);
-static int find_update(view_t *view, int backward);
+static void set_pos(int pos);
 static void goto_pos_force_update(int pos);
 static void goto_pos(int pos);
 static void update_ui(void);
 static int move_pos(int pos);
+static void handle_mouse_event(key_info_t key_info, keys_info_t *keys_info);
 
 static view_t *view;
 static int start_pos;
@@ -274,6 +277,7 @@ static keys_add_info_t builtin_cmds[] = {
 	{{K(KEY_UP)},      {{&cmd_k},      .descr = "go to item above"}},
 	{{K(KEY_RIGHT)},   {{&cmd_l},      .descr = "open selection/go to item to the right"}},
 #endif /* ENABLE_EXTENDED_KEYS */
+	{{K(KEY_MOUSE)}, {{&handle_mouse_event}, FOLLOWED_BY_NONE}},
 };
 
 void
@@ -392,7 +396,7 @@ cmd_ctrl_a(key_info_t key_info, keys_info_t *keys_info)
 static void
 cmd_ctrl_b(key_info_t key_info, keys_info_t *keys_info)
 {
-	if(can_scroll_up(view))
+	if(fpos_can_scroll_back(view))
 	{
 		page_scroll(fpos_get_last_visible_cell(view), -1);
 	}
@@ -416,10 +420,10 @@ cmd_ctrl_d(key_info_t key_info, keys_info_t *keys_info)
 static void
 cmd_ctrl_e(key_info_t key_info, keys_info_t *keys_info)
 {
-	if(fpos_has_hidden_top(view))
+	if(fpos_can_scroll_fwd(view))
 	{
-		int new_pos = get_corrected_list_pos_down(view, view->column_count);
-		scroll_down(view, view->column_count);
+		int new_pos = fpos_adjust_for_scroll_back(view, view->run_size);
+		fview_scroll_fwd_by(view, view->run_size);
 		goto_pos_force_update(new_pos);
 	}
 }
@@ -427,7 +431,7 @@ cmd_ctrl_e(key_info_t key_info, keys_info_t *keys_info)
 static void
 cmd_ctrl_f(key_info_t key_info, keys_info_t *keys_info)
 {
-	if(can_scroll_down(view))
+	if(fpos_can_scroll_fwd(view))
 	{
 		page_scroll(view->top_line, 1);
 	}
@@ -438,6 +442,7 @@ cmd_ctrl_f(key_info_t key_info, keys_info_t *keys_info)
 static void
 page_scroll(int base, int direction)
 {
+	/* TODO: deduplicate with fpos_scroll_page(). */
 	enum { HOR_GAP_SIZE = 2, VER_GAP_SIZE = 1 };
 	int offset = fview_is_transposed(view)
 	           ? MAX(1, (view->column_count - VER_GAP_SIZE))*view->window_rows
@@ -445,7 +450,7 @@ page_scroll(int base, int direction)
 	int new_pos = base + direction*offset
 	            + view->list_pos%view->run_size - base%view->run_size;
 	new_pos = MAX(0, MIN(view->list_rows - 1, new_pos));
-	scroll_by_files(view, direction*offset);
+	fview_scroll_by(view, direction*offset);
 	goto_pos(new_pos);
 }
 
@@ -463,7 +468,7 @@ static void
 cmd_ctrl_l(key_info_t key_info, keys_info_t *keys_info)
 {
 	update_screen(UT_FULL);
-	curs_set(0);
+	ui_set_cursor(/*visibility=*/0);
 }
 
 static void
@@ -506,10 +511,10 @@ call_incdec(int count)
 static void
 cmd_ctrl_y(key_info_t key_info, keys_info_t *keys_info)
 {
-	if(fpos_has_hidden_top(view))
+	if(fpos_can_scroll_back(view))
 	{
-		int new_pos = get_corrected_list_pos_up(view, view->column_count);
-		scroll_up(view, view->column_count);
+		int new_pos = fpos_adjust_for_scroll_fwd(view, view->run_size);
+		fview_scroll_back_by(view, view->run_size);
 		goto_pos_force_update(new_pos);
 	}
 }
@@ -574,7 +579,7 @@ cmd_M(key_info_t key_info, keys_info_t *keys_info)
 static void
 cmd_N(key_info_t key_info, keys_info_t *keys_info)
 {
-	search(key_info, !curr_stats.last_search_backward, 0);
+	search(key_info, !curr_stats.last_search_backward);
 }
 
 static void
@@ -658,7 +663,7 @@ cmd_colon(key_info_t key_info, keys_info_t *keys_info)
 {
 	update_marks(view);
 	cmds_vars_set_count(key_info.count);
-	modcline_enter(CLS_COMMAND, "", NULL);
+	modcline_enter(CLS_COMMAND, "");
 }
 
 /* Continues navigation to word which starts with specified character in initial
@@ -978,43 +983,14 @@ cmd_m(key_info_t key_info, keys_info_t *keys_info)
 static void
 cmd_n(key_info_t key_info, keys_info_t *keys_info)
 {
-	search(key_info, curr_stats.last_search_backward, 0);
+	search(key_info, curr_stats.last_search_backward);
 }
 
 static void
-search(key_info_t key_info, int backward, int interactive)
+search(key_info_t key_info, int backward)
 {
-	/* TODO: extract common part of this function and normal.c:search(). */
-
-	int found;
-
-	if(hist_is_empty(&curr_stats.search_hist))
-	{
-		return;
-	}
-
-	if(view->matches == 0)
-	{
-		const char *const pattern = hists_search_last();
-		curr_stats.save_msg = modvis_find(view, pattern, backward, interactive);
-		return;
-	}
-
-	if(key_info.count == NO_COUNT_GIVEN)
-		key_info.count = 1;
-	found = 0;
-	while(key_info.count-- > 0)
-		found += find_update(view, backward) != 0;
-
-	if(!found)
-	{
-		print_search_fail_msg(view, backward);
-		curr_stats.save_msg = 1;
-		return;
-	}
-
-	print_search_next_msg(view, backward);
-	curr_stats.save_msg = 1;
+	curr_stats.save_msg = search_next(curr_view, backward, /*stash_selection=*/0,
+			/*select_matches=*/0, def_count(key_info.count), &goto_pos);
 }
 
 /* Runs external editor to get command-line command and then executes it. */
@@ -1022,7 +998,7 @@ static void
 cmd_q_colon(key_info_t key_info, keys_info_t *keys_info)
 {
 	leave_clearing_selection(0, 0);
-	get_and_execute_command("", 0U, CIT_COMMAND);
+	cmds_run_ext("", 0U, CIT_COMMAND);
 }
 
 /* Runs external editor to get search pattern and then executes it. */
@@ -1045,17 +1021,17 @@ activate_search(int count, int back, int external)
 {
 	/* TODO: generalize with normal.c:activate_search(). */
 
-	search_repeat = (count == NO_COUNT_GIVEN) ? 1 : count;
+	search_repeat = def_count(count);
 	curr_stats.last_search_backward = back;
 	if(external)
 	{
 		CmdInputType type = back ? CIT_VBSEARCH_PATTERN : CIT_VFSEARCH_PATTERN;
-		get_and_execute_command("", 0U, type);
+		cmds_run_ext("", 0U, type);
 	}
 	else
 	{
 		const CmdLineSubmode submode = back ? CLS_VBSEARCH : CLS_VFSEARCH;
-		modcline_enter(submode, "", NULL);
+		modcline_enter(submode, "");
 	}
 }
 
@@ -1074,30 +1050,12 @@ cmd_v(key_info_t key_info, keys_info_t *keys_info)
 	}
 }
 
-/* Changes amend type and smartly reselects files. */
+/* Changes amend type and updates selected files. */
 static void
 change_amend_type(AmendType new_amend_type)
 {
-	const int cursor_pos = view->list_pos;
 	amend_type = new_amend_type;
-	view->list_pos = start_pos;
-
-	if(new_amend_type == AT_NONE)
-	{
-		flist_sel_stash(view);
-		/* All selection flags are reset, so this call actually clears the
-		 * backup. */
-		backup_selection_flags(view);
-	}
-	else
-	{
-		restore_selection_flags(view);
-	}
-
-	select_first_one();
-	move_pos(cursor_pos);
-
-	update_ui();
+	modvis_update();
 }
 
 /* Yanks files. */
@@ -1445,53 +1403,51 @@ revert_selection(int pos)
 void
 modvis_update(void)
 {
-	const int pos = view->list_pos;
+	const int cursor_pos = view->list_pos;
+	view->list_pos = start_pos;
 
-	flist_sel_stash(view);
-	view->dir_entry[start_pos].selected = 1;
-	view->selected_files = 1;
+	/* Regardless of the mode we've switched to, start with the original state
+	 * of selection to have consistent behaviour on `av` and `vav`. */
+	restore_selection_flags(view);
 
-	view->list_pos = start_pos;
-	goto_pos(pos);
+	if(amend_type == AT_NONE)
+	{
+		/* When not amending, original selection doesn't affect visual selection. */
+		flist_sel_stash(view);
+
+		/* All selection flags are reset, so this call actually clears the
+		 * backup. */
+		backup_selection_flags(view);
+	}
+
+	select_first_one();
+	move_pos(cursor_pos);
 
 	update_ui();
 }
 
 int
-modvis_find(view_t *view, const char pattern[], int backward, int print_errors)
+modvis_find(view_t *view, const char pattern[], int backward, int print_msg,
+		int *found)
 {
-	int i;
-	int result;
-	const int hls = cfg.hl_search;
-	int found;
-
-	cfg.hl_search = 0;
-	result = find_pattern(view, pattern, backward, 0, &found, print_errors);
-	if(!print_errors && result < 0)
-	{
-		/* If we're not printing messages, we might be interested in broken
-		 * pattern. */
-		return -1;
-	}
+	return search_find(view, pattern, backward, /*stash_selection=*/0,
+			/*select_matches=*/0, search_repeat, &goto_pos, print_msg, found);
+}
 
-	cfg.hl_search = hls;
-	for(i = 0; i < search_repeat; ++i)
-	{
-		find_update(view, backward);
-	}
-	return result;
+int
+modvis_find_interactive(view_t *view, const char pattern[], int backward,
+		int *found)
+{
+	int save_msg = search_find(view, pattern, backward, /*stash_selection=*/0,
+			/*select_matches=*/0, search_repeat, &set_pos, /*print_msg=*/0, found);
+	modvis_update();
+	return save_msg;
 }
 
-/* returns non-zero when it finds something */
-static int
-find_update(view_t *view, int backward)
+static void
+set_pos(int pos)
 {
-	const int old_pos = view->list_pos;
-	const int found = goto_search_match(view, backward);
-	const int new_pos = view->list_pos;
-	view->list_pos = old_pos;
-	goto_pos(new_pos);
-	return found;
+	view->list_pos = pos;
 }
 
 /* Moves cursor from its current position to specified pos selecting or
@@ -1559,5 +1515,52 @@ modvis_describe(void)
 	return descriptions[amend_type];
 }
 
+int
+modvis_is_amending(void)
+{
+	return vle_mode_is(VISUAL_MODE) && amend_type != AT_NONE;
+}
+
+/* Processes events from the mouse. */
+static void
+handle_mouse_event(key_info_t key_info, keys_info_t *keys_info)
+{
+	MEVENT e;
+	if(ui_get_mouse(&e) != OK)
+	{
+		return;
+	}
+
+	if(!wenclose(view->win, e.y, e.x))
+	{
+		return;
+	}
+
+	if(e.bstate & BUTTON1_PRESSED)
+	{
+		wmouse_trafo(view->win, &e.y, &e.x, FALSE);
+
+		int list_pos = fview_map_coordinates(curr_view, e.x, e.y);
+		if(list_pos >= 0)
+		{
+			int old_pos = curr_view->list_pos;
+			goto_pos(list_pos);
+
+			if(curr_view->list_pos == old_pos)
+			{
+				cmd_gl(key_info, keys_info);
+			}
+		}
+	}
+	else if(e.bstate & BUTTON4_PRESSED)
+	{
+		cmd_ctrl_y(key_info, keys_info);
+	}
+	else if(e.bstate & (BUTTON2_PRESSED | BUTTON5_PRESSED))
+	{
+		cmd_ctrl_e(key_info, keys_info);
+	}
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 filetype=c : */
diff --git a/src/modes/visual.h b/src/modes/visual.h
index 8fd000e..acefab4 100644
--- a/src/modes/visual.h
+++ b/src/modes/visual.h
@@ -40,21 +40,29 @@ void modvis_enter(VisualSubmodes sub_mode);
 /* Leaves visual mode in various ways. */
 void modvis_leave(int save_msg, int goto_top, int clear_selection);
 
-/* Should be used to ask visual mode to redraw file list correctly.
- * Intended to be used after setting list position from side. */
+/* Reselects files and updates UI.  Should be used to ask visual mode to redraw
+ * file list correctly.  Intended to be used after setting list position or
+ * after changing amending type from the outside. */
 void modvis_update(void);
 
 /* Kind of callback to allow starting searches from the module and rely on other
- * modules.  Returns new value for status bar message flag, but when
- * print_errors isn't requested can return -1 to indicate issues with the
- * pattern. */
+ * modules.  Returns new value for status bar message flag, but when print_msg
+ * isn't requested can return -1 to indicate issues with the pattern. */
 int modvis_find(struct view_t *view, const char pattern[], int backward,
-		int print_errors);
+		int print_msg, int *found);
+
+/* Same as modvis_find() but specifically for interactive search. */
+int modvis_find_interactive(struct view_t *view, const char pattern[],
+		int backward, int *found);
 
 /* Formats concise description of current visual mode state.  Returns pointer
  * to a statically allocated buffer. */
 const char * modvis_describe(void);
 
+/* Checks whether visual amend mode is active.  Returns non-zero if it is,
+ * otherwise zero is returned. */
+int modvis_is_amending(void);
+
 #endif /* VIFM__MODES__VISUAL_H__ */
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/src/ops.c b/src/ops.c
index e97e862..47ac4a6 100644
--- a/src/ops.c
+++ b/src/ops.c
@@ -40,6 +40,7 @@
 #include "io/ioeta.h"
 #include "io/iop.h"
 #include "io/ior.h"
+#include "lua/vlua.h"
 #include "modes/dialogs/msg_dialog.h"
 #include "ui/cancellation.h"
 #include "utils/cancellation.h"
@@ -130,6 +131,7 @@ static OpsResult op_rmdir(ops_t *ops, void *data, const char src[],
 static OpsResult op_mkfile(ops_t *ops, void *data, const char src[],
 		const char dst[]);
 static int ops_uses_syscalls(const ops_t *ops);
+static ShellType ops_shell_type(const ops_t *ops);
 static OpsResult exec_io_op(ops_t *ops, IoRes (*func)(io_args_t *),
 		io_args_t *args, int cancellable);
 static int confirm_overwrite(io_args_t *args, const char src[],
@@ -142,6 +144,7 @@ static int ui_cancellation_hook(void *arg);
 #ifndef _WIN32
 static OpsResult run_operation_command(ops_t *ops, char cmd[], int cancellable);
 #endif
+static int ops_runs_in_bg(const ops_t *ops);
 static int bg_cancellation_hook(void *arg);
 static OpsResult result_from_code(int exit_code);
 
@@ -296,7 +299,28 @@ OpsResult
 perform_operation(OPS op, ops_t *ops, void *data, const char src[],
 		const char dst[])
 {
-	return op_funcs[op](ops, data, src, dst);
+	if(ops_runs_in_bg(ops))
+	{
+		/* Not reporting events from background jobs. */
+		return op_funcs[op](ops, data, src, dst);
+	}
+
+	int dir = 0;
+	if(ONE_OF(op, OP_MKDIR, OP_RMDIR))
+	{
+		dir = 1;
+	}
+	else if(!ONE_OF(op, OP_NONE, OP_USR, OP_MKFILE, OP_SYMLINK, OP_SYMLINK2))
+	{
+		dir = is_dir(src);
+	}
+
+	OpsResult status = op_funcs[op](ops, data, src, dst);
+	if(status == OPS_SUCCEEDED)
+	{
+		vlua_events_app_fsop(curr_stats.vlua, op, src, dst, data, dir);
+	}
+	return status;
 }
 
 static OpsResult
@@ -340,7 +364,7 @@ op_removesl(ops_t *ops, void *data, const char src[], const char dst[])
 		char cmd[2*PATH_MAX + 1];
 		const int cancellable = (data == NULL);
 
-		escaped = shell_like_escape(src, 0);
+		escaped = shell_arg_escape(src, ops_shell_type(ops));
 		if(escaped == NULL)
 		{
 			return OPS_FAILED;
@@ -352,9 +376,15 @@ op_removesl(ops_t *ops, void *data, const char src[], const char dst[])
 		LOG_INFO_MSG("Running trash command: \"%s\"", cmd);
 		return run_operation_command(ops, cmd, cancellable);
 #else
+		char *src_copy = strdup(src);
+		internal_to_system_slashes(src_copy);
+
+		char *escaped_src = shell_arg_escape(src_copy, ST_CMD);
+		free(src_copy);
+
 		char cmd[PATH_MAX*2 + 1];
-		snprintf(cmd, sizeof(cmd), "%s \"%s\"", delete_prg, src);
-		internal_to_system_slashes(cmd);
+		snprintf(cmd, sizeof(cmd), "%s %s", delete_prg, escaped_src);
+		free(escaped_src);
 
 		return result_from_code(os_system(cmd));
 #endif
@@ -367,7 +397,7 @@ op_removesl(ops_t *ops, void *data, const char src[], const char dst[])
 		char cmd[16 + PATH_MAX];
 		const int cancellable = data == NULL;
 
-		escaped = shell_like_escape(src, 0);
+		escaped = shell_arg_escape(src, ops_shell_type(ops));
 		if(escaped == NULL)
 			return OPS_FAILED;
 
@@ -484,8 +514,8 @@ op_cp(ops_t *ops, void *data, const char src[], const char dst[],
 		char cmd[6 + PATH_MAX*2 + 1];
 		const int cancellable = (data == NULL);
 
-		escaped_src = shell_like_escape(src, 0);
-		escaped_dst = shell_like_escape(dst, 0);
+		escaped_src = shell_arg_escape(src, ops_shell_type(ops));
+		escaped_dst = shell_arg_escape(dst, ops_shell_type(ops));
 		if(escaped_src == NULL || escaped_dst == NULL)
 		{
 			free(escaped_dst);
@@ -509,9 +539,22 @@ op_cp(ops_t *ops, void *data, const char src[], const char dst[],
 
 		if(is_dir(src))
 		{
+			char *src_copy = strdup(src);
+			internal_to_system_slashes(src_copy);
+
+			char *dst_copy = strdup(dst);
+			internal_to_system_slashes(dst_copy);
+
+			char *escaped_src = shell_arg_escape(src_copy, ST_CMD);
+			free(src_copy);
+
+			char *escaped_dst = shell_arg_escape(dst_copy, ST_CMD);
+			free(dst_copy);
+
 			char cmd[6 + PATH_MAX*2 + 1];
-			snprintf(cmd, sizeof(cmd), "xcopy \"%s\" \"%s\" ", src, dst);
-			internal_to_system_slashes(cmd);
+			snprintf(cmd, sizeof(cmd), "xcopy %s %s ", escaped_src, escaped_dst);
+			free(escaped_src);
+			free(escaped_dst);
 
 			if(is_vista_and_above())
 				strcat(cmd, "/B ");
@@ -598,8 +641,8 @@ op_mv(ops_t *ops, void *data, const char src[], const char dst[],
 			return OPS_FAILED;
 		}
 
-		escaped_src = shell_like_escape(src, 0);
-		escaped_dst = shell_like_escape(dst, 0);
+		escaped_src = shell_arg_escape(src, ops_shell_type(ops));
+		escaped_dst = shell_arg_escape(dst, ops_shell_type(ops));
 		if(escaped_src == NULL || escaped_dst == NULL)
 		{
 			free(escaped_dst);
@@ -651,7 +694,9 @@ op_mv(ops_t *ops, void *data, const char src[], const char dst[],
 		result = exec_io_op(ops, &ior_mv, &args, data == NULL);
 	}
 
-	if(result == OPS_SUCCEEDED)
+	/* Accounting for background jobs might take some kind of a queue of events
+	 * to handle both internal updates and app.fsop Lua event. */
+	if(result == OPS_SUCCEEDED && !ops_runs_in_bg(ops))
 	{
 		trash_file_moved(src, dst);
 		bmarks_file_moved(src, dst);
@@ -683,7 +728,7 @@ op_chown(ops_t *ops, void *data, const char src[], const char dst[])
 	char *escaped;
 	uid_t uid = (uid_t)(long)data;
 
-	escaped = shell_like_escape(src, 0);
+	escaped = shell_arg_escape(src, ops_shell_type(ops));
 	snprintf(cmd, sizeof(cmd), "chown -fR %u %s", uid, escaped);
 	free(escaped);
 
@@ -702,7 +747,7 @@ op_chgrp(ops_t *ops, void *data, const char src[], const char dst[])
 	char *escaped;
 	gid_t gid = (gid_t)(long)data;
 
-	escaped = shell_like_escape(src, 0);
+	escaped = shell_arg_escape(src, ops_shell_type(ops));
 	snprintf(cmd, sizeof(cmd), "chown -fR :%u %s", gid, escaped);
 	free(escaped);
 
@@ -720,7 +765,7 @@ op_chmod(ops_t *ops, void *data, const char src[], const char dst[])
 	char cmd[128 + PATH_MAX];
 	char *escaped;
 
-	escaped = shell_like_escape(src, 0);
+	escaped = shell_arg_escape(src, ops_shell_type(ops));
 	snprintf(cmd, sizeof(cmd), "chmod %s %s", (char *)data, escaped);
 	free(escaped);
 
@@ -734,7 +779,7 @@ op_chmodr(ops_t *ops, void *data, const char src[], const char dst[])
 	char cmd[128 + PATH_MAX];
 	char *escaped;
 
-	escaped = shell_like_escape(src, 0);
+	escaped = shell_arg_escape(src, ops_shell_type(ops));
 	snprintf(cmd, sizeof(cmd), "chmod -R %s %s", (char *)data, escaped);
 	free(escaped);
 
@@ -792,17 +837,29 @@ op_symlink(ops_t *ops, void *data, const char src[], const char dst[])
 {
 	if(!ops_uses_syscalls(ops))
 	{
-		char *escaped_src, *escaped_dst;
 		char cmd[6 + PATH_MAX*2 + 1];
 		OpsResult result;
 
-#ifndef _WIN32
-		escaped_src = shell_like_escape(src, 0);
-		escaped_dst = shell_like_escape(dst, 0);
-#else
-		escaped_src = strdup(enclose_in_dquotes(src, ops->shell_type));
-		escaped_dst = strdup(enclose_in_dquotes(dst, ops->shell_type));
-#endif
+		char *src_copy = strdup(src);
+		char *dst_copy = strdup(dst);
+		if(src_copy == NULL || dst_copy == NULL)
+		{
+			free(src_copy);
+			free(dst_copy);
+			return OPS_FAILED;
+		}
+
+		internal_to_system_slashes(src_copy);
+		internal_to_system_slashes(dst_copy);
+
+		ShellType shell_type = (get_env_type() == ET_UNIX) ? ops_shell_type(ops)
+		                                                   : ST_CMD;
+
+		char *escaped_src = shell_arg_escape(src_copy, shell_type);
+		free(src_copy);
+
+		char *escaped_dst = shell_arg_escape(dst_copy, shell_type);
+		free(dst_copy);
 
 		if(escaped_src == NULL || escaped_dst == NULL)
 		{
@@ -824,8 +881,17 @@ op_symlink(ops_t *ops, void *data, const char src[], const char dst[])
 			return OPS_FAILED;
 		}
 
-		snprintf(cmd, sizeof(cmd), "%s\\win_helper -s %s %s", exe_dir, escaped_src,
+		internal_to_system_slashes(exe_dir);
+
+		char helper[PATH_MAX + 2];
+		snprintf(helper, sizeof(helper), "%s\\win_helper", exe_dir);
+
+		char *escaped_helper = shell_arg_escape(helper, shell_type);
+
+		snprintf(cmd, sizeof(cmd), "%s -s %s %s", escaped_helper, escaped_src,
 				escaped_dst);
+		free(escaped_helper);
+
 		result = result_from_code(os_system(cmd));
 #endif
 
@@ -851,7 +917,7 @@ op_mkdir(ops_t *ops, void *data, const char src[], const char dst[])
 		char cmd[128 + PATH_MAX];
 		char *escaped;
 
-		escaped = shell_like_escape(src, 0);
+		escaped = shell_arg_escape(src, ops_shell_type(ops));
 		snprintf(cmd, sizeof(cmd), "mkdir %s %s", (data == NULL) ? "" : "-p",
 				escaped);
 		free(escaped);
@@ -908,7 +974,7 @@ op_rmdir(ops_t *ops, void *data, const char src[], const char dst[])
 		char cmd[128 + PATH_MAX];
 		char *escaped;
 
-		escaped = shell_like_escape(src, 0);
+		escaped = shell_arg_escape(src, ops_shell_type(ops));
 		snprintf(cmd, sizeof(cmd), "rmdir %s", escaped);
 		free(escaped);
 		LOG_INFO_MSG("Running rmdir command: \"%s\"", cmd);
@@ -936,7 +1002,7 @@ op_mkfile(ops_t *ops, void *data, const char src[], const char dst[])
 		char cmd[128 + PATH_MAX];
 		char *escaped;
 
-		escaped = shell_like_escape(src, 0);
+		escaped = shell_arg_escape(src, ops_shell_type(ops));
 		snprintf(cmd, sizeof(cmd), "touch %s", escaped);
 		free(escaped);
 		LOG_INFO_MSG("Running touch command: \"%s\"", cmd);
@@ -972,6 +1038,13 @@ ops_uses_syscalls(const ops_t *ops)
 	return ops == NULL ? cfg.use_system_calls : ops->use_system_calls;
 }
 
+/* Retrieves shell type for the operation.  Returns the type. */
+static ShellType
+ops_shell_type(const ops_t *ops)
+{
+	return (ops == NULL ? curr_stats.shell_type : (ShellType)ops->shell_type);
+}
+
 /* Executes i/o operation with some predefined pre/post actions.  Returns
  * status. */
 static OpsResult
@@ -993,7 +1066,7 @@ exec_io_op(ops_t *ops, IoRes (*func)(io_args_t *), io_args_t *args,
 
 	if(cancellable)
 	{
-		if(ops != NULL && ops->bg)
+		if(ops_runs_in_bg(ops))
 		{
 			args->cancellation.arg = ops->bg_op;
 			args->cancellation.hook = &bg_cancellation_hook;
@@ -1009,11 +1082,13 @@ exec_io_op(ops_t *ops, IoRes (*func)(io_args_t *), io_args_t *args,
 
 	curr_ops = ops;
 	OpsResult result = OPS_FAILED;
-	switch(func(args))
+	IoRes io_res = func(args);
+	switch(io_res)
 	{
 		case IO_RES_SUCCEEDED: result = OPS_SUCCEEDED; break;
 		case IO_RES_SKIPPED:   result = OPS_SKIPPED; break;
 		case IO_RES_FAILED:    result = OPS_FAILED; break;
+		case IO_RES_ABORTED:   result = OPS_FAILED; break;
 	}
 	curr_ops = NULL;
 
@@ -1024,6 +1099,11 @@ exec_io_op(ops_t *ops, IoRes (*func)(io_args_t *), io_args_t *args,
 
 	if(ops != NULL)
 	{
+		if(io_res == IO_RES_ABORTED)
+		{
+			ops->aborted = 1;
+		}
+
 		size_t len = (ops->errors == NULL) ? 0U : strlen(ops->errors);
 		char *const suffix = ioe_errlst_to_str(&args->result.errors);
 
@@ -1047,10 +1127,10 @@ confirm_overwrite(io_args_t *args, const char src[], const char dst[])
 {
 	/* TODO: think about adding "append" and "rename" options here. */
 	static const response_variant responses[] = {
-		{ .key = 'y', .descr = "[y]es", },
-		{ .key = 'Y', .descr = "[Y]es for all", },
-		{ .key = 'n', .descr = "[n]o", },
-		{ .key = 'N', .descr = "[N]o for all", },
+		{ .key = 'y', .descr = "[y]es/", },
+		{ .key = 'Y', .descr = "[Y]es for all/", },
+		{ .key = 'n', .descr = "[n]o/", },
+		{ .key = 'N', .descr = "[N]o for all/", },
 		{ },
 	};
 
@@ -1115,9 +1195,9 @@ static IoErrCbResult
 dispatch_error(io_args_t *args, const ioe_err_t *err)
 {
 	static const response_variant responses[] = {
-		{ .key = 'r', .descr = "[r]etry", },
-		{ .key = 'i', .descr = "[i]gnore", },
-		{ .key = 'I', .descr = "[I]gnore for all", },
+		{ .key = 'r', .descr = "[r]etry/", },
+		{ .key = 'i', .descr = "[i]gnore/", },
+		{ .key = 'I', .descr = "[I]gnore for all/", },
 		{ .key = 'a', .descr = "[a]bort", },
 		{ },
 	};
@@ -1168,7 +1248,14 @@ prompt_user(const io_args_t *args, const char title[], const char msg[],
 	/* Active cancellation conflicts with input processing by putting terminal in
 	 * a cooked mode. */
 	ui_cancellation_push_off();
-	const char response = curr_ops->choose(title, msg, variants);
+
+	const custom_prompt_t prompt = {
+		.title = title,
+		.message = msg,
+		.variants = variants,
+	};
+	char response = curr_ops->choose(&prompt);
+
 	ui_cancellation_pop();
 
 	return response;
@@ -1193,7 +1280,7 @@ run_operation_command(ops_t *ops, char cmd[], int cancellable)
 		return result_from_code(bg_and_wait_for_errors(cmd, &no_cancellation));
 	}
 
-	if(ops != NULL && ops->bg)
+	if(ops_runs_in_bg(ops))
 	{
 		const cancellation_t bg_cancellation_info = {
 			.arg = ops->bg_op,
@@ -1215,6 +1302,14 @@ run_operation_command(ops_t *ops, char cmd[], int cancellable)
 
 #endif
 
+/* Checks whether operation is a background one.  The parameter can be NULL.
+ * Returns non-zero for background operations. */
+static int
+ops_runs_in_bg(const ops_t *ops)
+{
+	return (ops != NULL && ops->bg);
+}
+
 /* Implementation of cancellation hook for background tasks. */
 static int
 bg_cancellation_hook(void *arg)
diff --git a/src/ops.h b/src/ops.h
index df992c3..a058ff0 100644
--- a/src/ops.h
+++ b/src/ops.h
@@ -26,8 +26,8 @@ typedef enum
 {
 	OP_NONE,
 	OP_USR,
-	OP_REMOVE,   /* rm -rf */
-	OP_REMOVESL, /* cl */
+	OP_REMOVE,   /* rm -rf (after confirmation if necessary) */
+	OP_REMOVESL, /* rm -rf (unconditional; was added along with OP_SYMLINK2) */
 	OP_COPY,     /* copy and clone */
 	OP_COPYF,    /* copy with file overwrite */
 	OP_COPYA,    /* copy with appending to existing contents of destination */
@@ -82,11 +82,10 @@ typedef enum
 }
 ErrorResolutionPolicy;
 
-struct response_variant;
+struct custom_prompt_t;
 
-/* Function to choose an option.  Returns choice. */
-typedef char (*ops_choice_func)(const char title[], const char message[],
-		const struct response_variant *variants);
+/* Function to choose an option.  Returns the choice. */
+typedef char (*ops_choice_func)(const struct custom_prompt_t *details);
 
 /* Asks user to confirm some action by answering "Yes" or "No".  Returns
  * non-zero when user answers yes, otherwise zero is returned. */
@@ -94,7 +93,7 @@ typedef int (*ops_confirm_func)(const char title[], const char message[]);
 
 /* Description of file operation on a set of files.  Collects information and
  * helps to keep track of progress. */
-typedef struct
+typedef struct ops_t
 {
 	OPS main_op;           /* Primary operation performed on items. */
 	int total;             /* Total number of items to be processed. */
@@ -107,6 +106,7 @@ typedef struct
 	int bg;                /* Executed in background (no user interaction). */
 	struct bg_op_t *bg_op; /* Information for background operation. */
 	char *errors;          /* Multi-line string of errors. */
+	int aborted;           /* Processing should be stopped. */
 
 	/* It's unsafe to access global cfg object from threads performing background
 	 * operations, so copy them and use the copies. */
diff --git a/src/opt_handlers.c b/src/opt_handlers.c
index f51bddd..f655da5 100644
--- a/src/opt_handlers.c
+++ b/src/opt_handlers.c
@@ -94,6 +94,8 @@ static void init_lsoptions(optval_t *val);
 static void init_lsview(optval_t *val);
 static void init_milleroptions(optval_t *val);
 static void init_millerview(optval_t *val);
+static void init_mouse(optval_t *val);
+static void init_navoptions(optval_t *val);
 static void init_previewoptions(optval_t *val);
 static void init_quickview(optval_t *val);
 static void init_shortmess(optval_t *val);
@@ -151,6 +153,8 @@ static void mediaprg_handler(OPT_OP op, optval_t val);
 #endif
 static void mintimeoutlen_handler(OPT_OP op, optval_t val);
 static void scroll_line_down(view_t *view);
+static void mouse_handler(OPT_OP op, optval_t val);
+static void navoptions_handler(OPT_OP op, optval_t val);
 static void previewoptions_handler(OPT_OP op, optval_t val);
 static void quickview_handler(OPT_OP op, optval_t val);
 static void rulerformat_handler(OPT_OP op, optval_t val);
@@ -172,15 +176,17 @@ static void dotfiles_global(OPT_OP op, optval_t val);
 static void dotfiles_local(OPT_OP op, optval_t val);
 static void lsoptions_global(OPT_OP op, optval_t val);
 static void lsoptions_local(OPT_OP op, optval_t val);
-static void set_lsoptions(int *transposed, optval_t val, OPT_SCOPE scope);
-static void fill_lsoptions(optval_t *val, int transposed);
+static void set_lsoptions(int *transposed, int *column_count, optval_t val,
+		OPT_SCOPE scope);
+static void fill_lsoptions(optval_t *val, int transposed, int column_count);
 static void lsview_global(OPT_OP op, optval_t val);
 static void lsview_local(OPT_OP op, optval_t val);
 static void milleroptions_global(OPT_OP op, optval_t val);
 static void milleroptions_local(OPT_OP op, optval_t val);
-static void set_milleroptions(int ratios[3], int *preview_files, optval_t val,
-		OPT_SCOPE scope);
-static void fill_milleroptions(optval_t *val, int ratios[3], int preview_files);
+static void set_milleroptions(int ratios[3], MillerPreview *preview,
+		optval_t val, OPT_SCOPE scope);
+static void fill_milleroptions(optval_t *val, int ratios[3],
+		MillerPreview preview);
 static void millerview_global(OPT_OP op, optval_t val);
 static void millerview_local(OPT_OP op, optval_t val);
 static void number_global(OPT_OP op, optval_t val);
@@ -219,6 +225,7 @@ static void reset_suggestoptions(void);
 static void syncregs_handler(OPT_OP op, optval_t val);
 static void syscalls_handler(OPT_OP op, optval_t val);
 static void tablabel_handler(OPT_OP op, optval_t val);
+static void tabline_handler(OPT_OP op, optval_t val);
 static void tabprefix_handler(OPT_OP op, optval_t val);
 static void tabscope_handler(OPT_OP op, optval_t val);
 static void tabstop_handler(OPT_OP op, optval_t val);
@@ -330,7 +337,25 @@ ARRAY_GUARD(histcursor_vals, NUM_CHPOS);
 
 /* Possible keys of 'lsoptions' option. */
 static const char *lsoptions_enum[][2] = {
-	{ "transposed", "fill grid by column instead of by line" },
+	{ "columncount:", "fixed number of columns to display or 0" },
+	{ "transposed",   "fill grid by column instead of by line" },
+};
+
+/* Possible values of 'mouse'. */
+static const char *mouse_vals[][2] = {
+	{ "acmnqv", "all mouse values" },
+	{ "a", "all supported modes" },
+	{ "c", "command-line mode" },
+	{ "m", "menu mode" },
+	{ "n", "normal mode" },
+	{ "q", "view mode" },
+	{ "v", "visual mode" },
+};
+ARRAY_GUARD(mouse_vals, 1 + NUM_M_OPTS);
+
+/* Possible values of 'navoptions'. */
+static const char *navoptions_vals[][2] = {
+	{ "open:", "what entries to open on enter: dirs or all" },
 };
 
 /* Possible values of 'previewoptions'. */
@@ -420,7 +445,7 @@ static const char *milleroptions_enum[][2] = {
 	{ "lsize:",    "proportion of space given to the left column" },
 	{ "csize:",    "proportion of space given to the center column" },
 	{ "rsize:",    "proportion of space given to the right column" },
-	{ "rpreview:", "what should right pane preview: dirs or all" },
+	{ "rpreview:", "what should right pane preview: dirs, files or all" },
 };
 
 /* Possible keys of 'sizefmt' option. */
@@ -723,6 +748,15 @@ options[] = {
 	  OPT_INT, 0, NULL, &mintimeoutlen_handler, NULL,
 	  { .ref.int_val = &cfg.min_timeout_len },
 	},
+	{ "mouse", "", "which modes handle mouse",
+	  OPT_CHARSET, ARRAY_LEN(mouse_vals), mouse_vals, &mouse_handler, NULL,
+	  { .init = &init_mouse },
+	},
+	{ "navoptions", "", "tweaks for navigation mode",
+	  OPT_STRLIST, ARRAY_LEN(navoptions_vals), navoptions_vals,
+	  &navoptions_handler, NULL,
+	  { .init = &init_navoptions },
+	},
 	{ "previewoptions", "", "tweaks for how preview is done",
 	  OPT_STRLIST, ARRAY_LEN(previewoptions_vals), previewoptions_vals,
 		&previewoptions_handler, NULL,
@@ -809,6 +843,10 @@ options[] = {
 	  OPT_STR, 0, NULL, &tablabel_handler, NULL,
 	  { .ref.str_val = &cfg.tab_label },
 	},
+	{ "tabline", "tal", "format of the tab line",
+	  OPT_STR, 0, NULL, &tabline_handler, NULL,
+	  { .ref.str_val = &cfg.tab_line },
+	},
 	{ "tabprefix", "", "format of prefix of a tab's label",
 	  OPT_STR, 0, NULL, &tabprefix_handler, NULL,
 	  { .ref.str_val = &cfg.tab_prefix },
@@ -1171,7 +1209,7 @@ init_dotfiles(optval_t *val)
 static void
 init_lsoptions(optval_t *val)
 {
-	fill_lsoptions(val, curr_view->ls_transposed_g);
+	fill_lsoptions(val, curr_view->ls_transposed_g, curr_view->ls_cols_g);
 }
 
 /* Initializes 'lsview' option from global value. */
@@ -1186,7 +1224,7 @@ static void
 init_milleroptions(optval_t *val)
 {
 	fill_milleroptions(val, curr_view->miller_ratios_g,
-			curr_view->miller_preview_files_g);
+			curr_view->miller_preview_g);
 }
 
 /* Initializes 'millerview' option from global value. */
@@ -1196,6 +1234,31 @@ init_millerview(optval_t *val)
 	val->bool_val = curr_view->miller_view_g;
 }
 
+/* Initializes 'mouse' option from global state. */
+static void
+init_mouse(optval_t *val)
+{
+	static char buf[32];
+	snprintf(buf, sizeof(buf), "%s%s%s%s%s%s",
+			cfg.mouse & M_ALL_MODES ? "a" : "",
+			cfg.mouse & M_CMDLINE_MODE ? "c" : "",
+			cfg.mouse & M_MENU_MODE ? "m" : "",
+			cfg.mouse & M_NORMAL_MODE ? "n" : "",
+			cfg.mouse & M_VIEW_MODE ? "q" : "",
+			cfg.mouse & M_VISUAL_MODE ? "v" : "");
+	val->str_val = buf;
+}
+
+/* Initializes 'navoptions' option from global state. */
+static void
+init_navoptions(optval_t *val)
+{
+	static char buf[16];
+	snprintf(buf, sizeof(buf), "open:%s", (cfg.nav_open_files ? "all" : "dirs"));
+
+	val->str_val = buf;
+}
+
 /* Initializes 'previewoptions' option from global state. */
 static void
 init_previewoptions(optval_t *val)
@@ -1422,16 +1485,17 @@ reset_local_options(view_t *view)
 	vle_opts_assign("dotfiles", val, OPT_LOCAL);
 
 	view->ls_transposed = view->ls_transposed_g;
-	fill_lsoptions(&val, view->ls_transposed_g);
+	fill_lsoptions(&val, view->ls_transposed_g, view->ls_cols_g);
 	vle_opts_assign("lsoptions", val, OPT_LOCAL);
 
 	fview_set_lsview(view, view->ls_view_g);
 	val.int_val = view->ls_view_g;
 	vle_opts_assign("lsview", val, OPT_LOCAL);
 
+	view->miller_preview = view->miller_preview_g;
 	memcpy(view->miller_ratios, view->miller_ratios_g,
 			sizeof(view->miller_ratios));
-	fill_milleroptions(&val, view->miller_ratios_g, view->miller_preview_files_g);
+	fill_milleroptions(&val, view->miller_ratios_g, view->miller_preview_g);
 	vle_opts_assign("milleroptions", val, OPT_LOCAL);
 
 	fview_set_millerview(view, view->miller_view_g);
@@ -1484,9 +1548,9 @@ load_view_options(view_t *view)
 	val.bool_val = !view->hide_dot_g;
 	vle_opts_assign("dotfiles", val, OPT_GLOBAL);
 
-	fill_lsoptions(&val, view->ls_transposed);
+	fill_lsoptions(&val, view->ls_transposed, view->ls_cols);
 	vle_opts_assign("lsoptions", val, OPT_LOCAL);
-	fill_lsoptions(&val, view->ls_transposed_g);
+	fill_lsoptions(&val, view->ls_transposed_g, view->ls_cols_g);
 	vle_opts_assign("lsoptions", val, OPT_GLOBAL);
 
 	val.bool_val = view->ls_view;
@@ -1494,9 +1558,9 @@ load_view_options(view_t *view)
 	val.bool_val = view->ls_view_g;
 	vle_opts_assign("lsview", val, OPT_GLOBAL);
 
-	fill_milleroptions(&val, view->miller_ratios, view->miller_preview_files);
+	fill_milleroptions(&val, view->miller_ratios, view->miller_preview);
 	vle_opts_assign("milleroptions", val, OPT_LOCAL);
-	fill_milleroptions(&val, view->miller_ratios_g, view->miller_preview_files_g);
+	fill_milleroptions(&val, view->miller_ratios_g, view->miller_preview_g);
 	vle_opts_assign("milleroptions", val, OPT_GLOBAL);
 
 	val.bool_val = view->miller_view;
@@ -1562,6 +1626,9 @@ clone_local_options(const view_t *from, view_t *to, int defer_slow)
 	to->miller_view_g = from->miller_view_g;
 	fview_set_millerview(to, from->miller_view);
 
+	to->miller_preview_g = from->miller_preview_g;
+	to->miller_preview = from->miller_preview;
+
 	replace_string(&to->preview_prg, from->preview_prg);
 	replace_string(&to->preview_prg_g, from->preview_prg_g);
 }
@@ -2320,6 +2387,83 @@ scroll_line_down(view_t *view)
 	wresize(view->win, view->window_rows, view->window_cols);
 }
 
+/* Handles updates of the 'mouse' option. */
+static void
+mouse_handler(OPT_OP op, optval_t val)
+{
+	int mouse = 0;
+
+	const char *p;
+	for(p = val.str_val; *p != '\0'; ++p)
+	{
+		switch(*p)
+		{
+			case 'a': mouse |= M_ALL_MODES; break;
+			case 'c': mouse |= M_CMDLINE_MODE; break;
+			case 'm': mouse |= M_MENU_MODE; break;
+			case 'n': mouse |= M_NORMAL_MODE; break;
+			case 'q': mouse |= M_VIEW_MODE; break;
+			case 'v': mouse |= M_VISUAL_MODE; break;
+
+			default:
+				assert(0 && "Unhandled mouse flag.");
+				break;
+		}
+	}
+
+	/* Update terminal state only if there is a need for it. */
+	if((cfg.mouse == 0) != (mouse == 0))
+	{
+		ui_set_mouse_active(mouse != 0);
+	}
+
+	cfg.mouse = mouse;
+}
+
+/* Handles updates of the 'navoptions' option. */
+static void
+navoptions_handler(OPT_OP op, optval_t val)
+{
+	char *new_val = strdup(val.str_val);
+	char *part = new_val, *state = NULL;
+
+	int open_all = 0;
+
+	while((part = split_and_get(part, ',', &state)) != NULL)
+	{
+		if(starts_with_lit(part, "open:"))
+		{
+			const char *const str = after_first(part, ':');
+			if(strcmp(str, "all") == 0)
+			{
+				open_all = 1;
+			}
+			else if(strcmp(str, "dirs") == 0)
+			{
+				open_all = 0;
+			}
+			else
+			{
+				vle_tb_append_linef(vle_err, "Failed to parse \"open\" value: %s", str);
+				break;
+			}
+		}
+		else
+		{
+			break_at(part, ':');
+			vle_tb_append_linef(vle_err, "Unknown key for 'navoptions' option: %s",
+					part);
+			break;
+		}
+	}
+	free(new_val);
+
+	if(part == NULL)
+	{
+		cfg.nav_open_files = open_all;
+	}
+}
+
 /* Handles updates of the 'previewoptions' option. */
 static void
 previewoptions_handler(OPT_OP op, optval_t val)
@@ -2430,7 +2574,7 @@ static void
 scrollbind_handler(OPT_OP op, optval_t val)
 {
 	cfg.scroll_bind = val.bool_val;
-	update_scroll_bind_offset();
+	ui_remember_scroll_offset();
 }
 
 static void
@@ -2642,31 +2786,49 @@ dotfiles_local(OPT_OP op, optval_t val)
 static void
 lsoptions_global(OPT_OP op, optval_t val)
 {
-	set_lsoptions(&curr_view->ls_transposed_g, val, OPT_GLOBAL);
+	set_lsoptions(&curr_view->ls_transposed_g, &curr_view->ls_cols_g, val,
+			OPT_GLOBAL);
 }
 
 /* Handles update of ls settings as a local option. */
 static void
 lsoptions_local(OPT_OP op, optval_t val)
 {
-	set_lsoptions(&curr_view->ls_transposed, val, OPT_LOCAL);
+	set_lsoptions(&curr_view->ls_transposed, &curr_view->ls_cols, val, OPT_LOCAL);
 }
 
 /* Handles update of ls-view settings. */
 static void
-set_lsoptions(int *transposed, optval_t val, OPT_SCOPE scope)
+set_lsoptions(int *transposed, int *column_count, optval_t val, OPT_SCOPE scope)
 {
 	char *new_val = strdup(val.str_val);
 	char *part = new_val, *state = NULL;
 
 	int transpose = 0;
+	int fixed_columns = 0;
 
 	while((part = split_and_get(part, ',', &state)) != NULL)
 	{
-		if(strcmp(part, "transposed") == 0)
+		const char *option = part;
+		if(strcmp(option, "transposed") == 0)
 		{
 			transpose = 1;
 		}
+		else if(skip_prefix(&option, "columncount:"))
+		{
+			if(!read_int(option, &fixed_columns))
+			{
+				vle_tb_append_linef(vle_err,
+						"Failed to parse \"columncount\" value: %s", option);
+				break;
+			}
+			if(fixed_columns < 0)
+			{
+				vle_tb_append_linef(vle_err,
+						"\"columncount\" can't be less than 0, got: %s", option);
+				break;
+			}
+		}
 		else
 		{
 			break_at(part, ':');
@@ -2680,6 +2842,7 @@ set_lsoptions(int *transposed, optval_t val, OPT_SCOPE scope)
 	if(part == NULL)
 	{
 		*transposed = transpose;
+		*column_count = fixed_columns;
 
 		if(!ui_view_displays_columns(curr_view) &&
 				transposed == &curr_view->ls_transposed)
@@ -2688,16 +2851,17 @@ set_lsoptions(int *transposed, optval_t val, OPT_SCOPE scope)
 		}
 	}
 
-	fill_lsoptions(&val, *transposed);
+	fill_lsoptions(&val, *transposed, *column_count);
 	vle_opts_assign("lsoptions", val, scope);
 }
 
 /* Loads value of lsoptions as a string. */
 static void
-fill_lsoptions(optval_t *val, int transposed)
+fill_lsoptions(optval_t *val, int transposed, int column_count)
 {
 	static char buf[64];
-	copy_str(buf, sizeof(buf), transposed ? "transposed" : "");
+	snprintf(buf, sizeof(buf), "columncount:%d%s", column_count,
+			transposed ? ",transposed" : "");
 	val->str_val = buf;
 }
 
@@ -2719,28 +2883,28 @@ lsview_local(OPT_OP op, optval_t val)
 static void
 milleroptions_global(OPT_OP op, optval_t val)
 {
-	set_milleroptions(curr_view->miller_ratios_g,
-			&curr_view->miller_preview_files_g, val, OPT_GLOBAL);
+	set_milleroptions(curr_view->miller_ratios_g, &curr_view->miller_preview_g,
+			val, OPT_GLOBAL);
 }
 
 /* Handles update of miller columns settings as a local option. */
 static void
 milleroptions_local(OPT_OP op, optval_t val)
 {
-	set_milleroptions(curr_view->miller_ratios, &curr_view->miller_preview_files,
-			val, OPT_LOCAL);
+	set_milleroptions(curr_view->miller_ratios, &curr_view->miller_preview, val,
+			OPT_LOCAL);
 }
 
 /* Handles update of miller columns settings. */
 static void
-set_milleroptions(int ratios[3], int *preview_files, optval_t val,
+set_milleroptions(int ratios[3], MillerPreview *preview, optval_t val,
 		OPT_SCOPE scope)
 {
 	char *new_val = strdup(val.str_val);
 	char *part = new_val, *state = NULL;
 
 	int lsize = 0, csize = 1, rsize = 0;
-	int preview_all = 0;
+	MillerPreview preview_what = MP_DIRS;
 
 	while((part = split_and_get(part, ',', &state)) != NULL)
 	{
@@ -2785,11 +2949,15 @@ set_milleroptions(int ratios[3], int *preview_files, optval_t val,
 			const char *const str = after_first(part, ':');
 			if(strcmp(str, "all") == 0)
 			{
-				preview_all = 1;
+				preview_what = MP_ALL;
 			}
 			else if(strcmp(str, "dirs") == 0)
 			{
-				preview_all = 0;
+				preview_what = MP_DIRS;
+			}
+			else if(strcmp(str, "files") == 0)
+			{
+				preview_what = MP_FILES;
 			}
 			else
 			{
@@ -2813,7 +2981,7 @@ set_milleroptions(int ratios[3], int *preview_files, optval_t val,
 		ratios[0] = MAX(0, MIN(100, lsize));
 		ratios[1] = MAX(0, MIN(100, csize));
 		ratios[2] = MAX(0, MIN(100, rsize));
-		*preview_files = preview_all;
+		*preview = preview_what;
 
 		if(ratios == curr_view->miller_ratios)
 		{
@@ -2821,18 +2989,21 @@ set_milleroptions(int ratios[3], int *preview_files, optval_t val,
 		}
 	}
 
-	fill_milleroptions(&val, ratios, preview_all);
+	fill_milleroptions(&val, ratios, *preview);
 	vle_opts_assign("milleroptions", val, scope);
 }
 
 /* Loads value of milleroptions as a string. */
 static void
-fill_milleroptions(optval_t *val, int ratios[3], int preview_files)
+fill_milleroptions(optval_t *val, int ratios[3], MillerPreview preview)
 {
 	static char buf[64];
+
+	const char *rpreview = (preview == MP_DIRS) ? "dirs"
+	                     : (preview == MP_FILES) ? "files" : "all";
+
 	snprintf(buf, sizeof(buf), "lsize:%d,csize:%d,rsize:%d,rpreview:%s",
-			ratios[0], ratios[1], ratios[2],
-			(preview_files ? "all" : "dirs"));
+			ratios[0], ratios[1], ratios[2], rpreview);
 	val->str_val = buf;
 }
 
@@ -3245,7 +3416,12 @@ map_name(const char name[], void *arg)
 		return (pos + 1);
 	}
 
-	return vlua_viewcolumn_map(curr_stats.vlua, name);
+	int id = vlua_viewcolumn_map(curr_stats.vlua, name);
+	if(id == -1)
+	{
+		vle_tb_append_linef(vle_err, "Failed to find column: %s", name);
+	}
+	return id;
 }
 
 void
@@ -3451,6 +3627,14 @@ tablabel_handler(OPT_OP op, optval_t val)
 	stats_redraw_later();
 }
 
+/* Sets format string for the whole tab line. */
+static void
+tabline_handler(OPT_OP op, optval_t val)
+{
+	(void)replace_string(&cfg.tab_line, val.str_val);
+	stats_redraw_later();
+}
+
 /* Sets format string for tab label's prefix. */
 static void
 tabprefix_handler(OPT_OP op, optval_t val)
@@ -3549,6 +3733,15 @@ trash_handler(OPT_OP op, optval_t val)
 static void
 trashdir_handler(OPT_OP op, optval_t val)
 {
+	if(curr_stats.restart_in_progress && op == OP_RESET)
+	{
+		/* This must be a valid value, just set it without checks.  It will be
+		 * applied at the end of the restart.  This avoids creation of unwanted
+		 * creation of the trash at default location. */
+		copy_str(cfg.trash_dir, sizeof(cfg.trash_dir), val.str_val);
+		return;
+	}
+
 	if(trash_set_specs(val.str_val) != 0)
 	{
 		/* Reset the 'trashdir' option to its previous value. */
diff --git a/src/plugins.c b/src/plugins.c
index d656355..1d5333a 100644
--- a/src/plugins.c
+++ b/src/plugins.c
@@ -49,8 +49,10 @@ struct plugs_t
 	int loaded; /* Whether plugins were loaded, shouldn't load them twice. */
 };
 
+static void load_plugs_dir(plugs_t *plugs, const char plugin_path[]);
 static void plug_free(plug_t *plug);
-static plug_t * find_plug(plugs_t *plugs, const char real_path[]);
+static plug_t * plug_from_name(plugs_t *plugs, const char name[]);
+static plug_t * plug_from_real_path(plugs_t *plugs, const char real_path[]);
 static int should_be_loaded(plugs_t *plugs, const char name[]);
 static void plug_logf(plug_t *plug, const char format[], ...)
 	_gnuc_printf(2, 3);
@@ -94,29 +96,37 @@ plugs_free(plugs_t *plugs)
 }
 
 void
-plugs_load(plugs_t *plugs, const char base_dir[])
+plugs_load(plugs_t *plugs, strlist_t plugins_dirs)
 {
 	if(plugs->loaded)
 	{
 		return;
 	}
 
-	char full_path[PATH_MAX + 1];
-	snprintf(full_path, sizeof(full_path), "%s/plugins", base_dir);
+	plugs->loaded = 1;
+
+	int i;
+	for(i = 0; i < plugins_dirs.nitems; ++i)
+	{
+		load_plugs_dir(plugs, plugins_dirs.items[i]);
+	}
+}
 
-	DIR *dir = os_opendir(full_path);
+/* Loads all plugins from a directory if it can be listed. */
+static void
+load_plugs_dir(plugs_t *plugs, const char plugin_path[])
+{
+	DIR *dir = os_opendir(plugin_path);
 	if(dir == NULL)
 	{
 		return;
 	}
 
-	plugs->loaded = 1;
-
 	struct dirent *entry;
 	while((entry = os_readdir(dir)) != NULL)
 	{
 		char path[PATH_MAX + NAME_MAX + 4];
-		snprintf(path, sizeof(path), "%s/%s", full_path, entry->d_name);
+		snprintf(path, sizeof(path), "%s/%s", plugin_path, entry->d_name);
 
 		/* XXX: is_dirent_targets_dir() does slowfs checks, do they harm here? */
 		if(entry->d_name[0] == '.' || is_builtin_dir(entry->d_name) ||
@@ -163,16 +173,23 @@ plugs_load(plugs_t *plugs, const char base_dir[])
 			continue;
 		}
 
-		/* Perform the check before committing this plugin. */
-		plug_t *duplicate = find_plug(plugs, plug->real_path);
+		/* Perform the searches before committing this plugin. */
+		plug_t *name_duplicate = plug_from_name(plugs, plug->name);
+		plug_t *real_duplicate = plug_from_real_path(plugs, plug->real_path);
 
 		plug->status = PLS_FAILURE;
 		DA_COMMIT(plugs->plugs);
 
-		if(duplicate != NULL)
+		if(real_duplicate != NULL)
 		{
 			plug_logf(plug, "[vifm][error]: skipped as a duplicate of %s",
-					duplicate->path);
+					real_duplicate->path);
+			plug->status = PLS_SKIPPED;
+		}
+		else if(name_duplicate != NULL)
+		{
+			plug_logf(plug, "[vifm][error]: skipped as a conflicting with %s",
+					name_duplicate->path);
 			plug->status = PLS_SKIPPED;
 		}
 		else if(!should_be_loaded(plugs, plug->name))
@@ -180,7 +197,7 @@ plugs_load(plugs_t *plugs, const char base_dir[])
 			plug_log(plug, "[vifm][info]: skipped due to blacklist/whitelist");
 			plug->status = PLS_SKIPPED;
 		}
-		else if(vlua_load_plugin(plugs->vlua, entry->d_name, plug) == 0)
+		else if(vlua_load_plugin(plugs->vlua, plug) == 0)
 		{
 			plug_log(plug, "[vifm][info]: plugin was loaded successfully");
 			plug->status = PLS_SUCCESS;
@@ -205,10 +222,26 @@ plug_free(plug_t *plug)
 	free(plug);
 }
 
+/* Looks up a plugin specified by name among already loaded plugins.  Returns
+ * pointer to it or NULL. */
+static plug_t *
+plug_from_name(plugs_t *plugs, const char name[])
+{
+	size_t i;
+	for(i = 0U; i < DA_SIZE(plugs->plugs); ++i)
+	{
+		if(strcasecmp(plugs->plugs[i]->name, name) == 0)
+		{
+			return plugs->plugs[i];
+		}
+	}
+	return NULL;
+}
+
 /* Looks up a plugin specified by real path among already loaded plugins.
  * Returns pointer to it or NULL. */
 static plug_t *
-find_plug(plugs_t *plugs, const char real_path[])
+plug_from_real_path(plugs_t *plugs, const char real_path[])
 {
 	size_t i;
 	for(i = 0U; i < DA_SIZE(plugs->plugs); ++i)
diff --git a/src/plugins.h b/src/plugins.h
index c7fb8c8..68be50f 100644
--- a/src/plugins.h
+++ b/src/plugins.h
@@ -49,6 +49,7 @@ typedef struct plug_t
 }
 plug_t;
 
+struct strlist_t;
 struct vlua_t;
 
 /* Creates new instance of the unit.  Returns the instance or NULL. */
@@ -57,8 +58,9 @@ plugs_t * plugs_create(struct vlua_t *vlua);
 /* Frees resources of the unit. */
 void plugs_free(plugs_t *plugs);
 
-/* Loads plugins. */
-void plugs_load(plugs_t *plugs, const char base_dir[]);
+/* Loads plugins from each of the passed in directories that can be listed.
+ * Immediately returns on all but first invocation. */
+void plugs_load(plugs_t *plugs, struct strlist_t plugins_dirs);
 
 /* Assigns *plug to information structure about a plugin specified by its index.
  * Returns non-zero on success, otherwise zero is returned. */
diff --git a/src/registers.c b/src/registers.c
index 5a8c5fb..494d38d 100644
--- a/src/registers.c
+++ b/src/registers.c
@@ -163,6 +163,7 @@ static unsigned int seen_generation;
 static int debug_print_to_stdout;
 
 static int find_in_reg(const reg_t *reg, const char file[]);
+static reg_t * reg_from_name(int reg_name);
 static void regs_sync_error(const char msg[]);
 static int regs_sync_to_shared_memory_critical(void);
 static int regs_sync_enter_critical_section(void);
@@ -196,18 +197,10 @@ regs_exists(int reg_name)
 	return char_is_one_of(valid_registers, reg_name);
 }
 
-reg_t *
+const reg_t *
 regs_find(int reg_name)
 {
-	int i;
-	for(i = 0; i < NUM_REGISTERS; ++i)
-	{
-		if(registers[i].name == reg_name)
-		{
-			return &registers[i];
-		}
-	}
-	return NULL;
+	return reg_from_name(reg_name);
 }
 
 int
@@ -218,7 +211,7 @@ regs_append(int reg_name, const char file[])
 		return 0;
 	}
 
-	reg_t *reg = regs_find(reg_name);
+	reg_t *reg = reg_from_name(reg_name);
 	if(reg == NULL)
 	{
 		return 1;
@@ -245,6 +238,51 @@ regs_append(int reg_name, const char file[])
 	return 0;
 }
 
+void
+regs_set(int reg_name, char **files, int nfiles)
+{
+	if(nfiles == 0)
+	{
+		regs_clear(reg_name);
+		return;
+	}
+
+	reg_t *const reg = reg_from_name(reg_name);
+	if(reg == NULL)
+	{
+		return;
+	}
+
+	files = copy_string_array(files, nfiles);
+	if(files == NULL)
+	{
+		return;
+	}
+
+	/* Registers are sorted. */
+	safe_qsort(files, nfiles, sizeof(*files), &strossorter);
+
+	/* And don't contain duplicates. */
+	int i;
+	int j = 1;
+	for(i = 1; i < nfiles; ++i)
+	{
+		if(stroscmp(files[i - 1], files[i]) == 0)
+		{
+			free(files[i]);
+		}
+		else
+		{
+			files[j++] = files[i];
+		}
+	}
+	nfiles = j;
+
+	free_string_array(reg->files, reg->nfiles);
+	reg->files = files;
+	reg->nfiles = nfiles;
+}
+
 void
 regs_reset(void)
 {
@@ -258,7 +296,7 @@ regs_reset(void)
 void
 regs_clear(int reg_name)
 {
-	reg_t *const reg = regs_find(reg_name);
+	reg_t *const reg = reg_from_name(reg_name);
 	if(reg == NULL)
 	{
 		return;
@@ -273,7 +311,7 @@ void
 regs_pack(int reg_name)
 {
 	int j, i;
-	reg_t *const reg = regs_find(reg_name);
+	reg_t *const reg = reg_from_name(reg_name);
 	if(reg == NULL)
 	{
 		return;
@@ -298,7 +336,7 @@ regs_list(const char registers[])
 
 	while(*registers != '\0')
 	{
-		reg_t *reg = regs_find(*registers++);
+		reg_t *reg = reg_from_name(*registers++);
 		char reg_str[16];
 		int i;
 
@@ -365,6 +403,22 @@ find_in_reg(const reg_t *reg, const char file[])
 	return -l - 1;
 }
 
+/* Retrieves register structure by register name.  Returns the structure or NULL
+ * if register name is incorrect. */
+static reg_t *
+reg_from_name(int reg_name)
+{
+	int i;
+	for(i = 0; i < NUM_REGISTERS; ++i)
+	{
+		if(registers[i].name == reg_name)
+		{
+			return &registers[i];
+		}
+	}
+	return NULL;
+}
+
 void
 regs_remove_trashed_files(const char trash_dir[])
 {
@@ -399,10 +453,10 @@ regs_update_unnamed(int reg_name)
 	if(reg_name == UNNAMED_REG_NAME)
 		return;
 
-	if((reg = regs_find(reg_name)) == NULL)
+	if((reg = reg_from_name(reg_name)) == NULL)
 		return;
 
-	if((unnamed = regs_find(UNNAMED_REG_NAME)) == NULL)
+	if((unnamed = reg_from_name(UNNAMED_REG_NAME)) == NULL)
 		return;
 
 	regs_clear(UNNAMED_REG_NAME);
@@ -424,7 +478,7 @@ regs_suggest(regs_suggest_cb cb, int max_entries_per_reg)
 
 	while(*registers != '\0')
 	{
-		reg_t *const reg = regs_find(*registers++);
+		reg_t *const reg = reg_from_name(*registers++);
 		int i, max = max_entries_per_reg;
 
 		if(reg == NULL || reg->nfiles <= 0)
diff --git a/src/registers.h b/src/registers.h
index 70ddfc9..ad9dc29 100644
--- a/src/registers.h
+++ b/src/registers.h
@@ -31,11 +31,12 @@
 #define BLACKHOLE_REG_NAME '_'
 
 /* Holds register data. */
-typedef struct
+typedef struct reg_t
 {
 	int name;     /* Name of the register. */
 	int nfiles;   /* Number of files in the register. */
-	char **files; /* List of full paths of files. */
+	char **files; /* List of full canonicalized paths that is sorted and
+	                 deduplicated according to case-sensitivity of the system. */
 }
 reg_t;
 
@@ -55,13 +56,16 @@ int regs_exists(int reg_name);
 
 /* Retrieves register structure by register name.  Returns the structure or NULL
  * if register name is incorrect. */
-reg_t * regs_find(int reg_name);
+const reg_t * regs_find(int reg_name);
 
 /* Appends path to the file to register specified by name.  Might fail for
  * duplicate, non-existing path or wrong register name.  Returns zero when file
  * is added, otherwise non-zero is returned. */
 int regs_append(int reg_name, const char file[]);
 
+/* Replaces contents of a register. */
+void regs_set(int reg_name, char **files, int nfiles);
+
 /* Clears all registers.  Pair of regs_init(). */
 void regs_reset(void);
 
diff --git a/src/running.c b/src/running.c
index a4bab25..b63c27d 100644
--- a/src/running.c
+++ b/src/running.c
@@ -19,8 +19,6 @@
 
 #include "running.h"
 
-#include <curses.h> /* FALSE curs_set() */
-
 #include <sys/stat.h> /* stat */
 #ifdef _WIN32
 #define WIN32_LEAN_AND_MEAN
@@ -106,7 +104,6 @@ static void run_implicit_prog(view_t *view, const char prog_spec[], int pause,
 		int force_bg);
 static void view_current_file(const view_t *view);
 static void follow_link(view_t *view, int follow_dirs, int ultimate);
-static void enter_dir(struct view_t *view);
 static int cd_to_parent_dir(view_t *view);
 static void extract_last_path_component(const char path[], char buf[]);
 static char * run_shell_prepare(const char command[], ShellPause pause,
@@ -186,7 +183,7 @@ handle_file(view_t *view, FileHandleExec exec, FileHandleLink follow)
 		int dir_like_entry = (is_dir(full_path) || is_unc_root(view->curr_dir));
 		if(dir_like_entry)
 		{
-			enter_dir(view);
+			(void)rn_enter_dir(view);
 			return;
 		}
 	}
@@ -371,17 +368,18 @@ static void
 execute_file(const char full_path[], int elevate)
 {
 #ifndef _WIN32
-	char *const escaped = shell_like_escape(full_path, 0);
+	char *const escaped = shell_arg_escape(full_path, curr_stats.shell_type);
 	rn_shell(escaped, PAUSE_ALWAYS, 1, SHELL_BY_APP);
 	free(escaped);
 #else
-	char *const dquoted_full_path =
-		strdup(enclose_in_dquotes(full_path, curr_stats.shell_type));
+	char *copy = strdup(full_path);
+	internal_to_system_slashes(copy);
 
-	internal_to_system_slashes(dquoted_full_path);
-	run_win_executable(dquoted_full_path, elevate);
+	char *escaped = shell_arg_escape(copy, curr_stats.shell_type);
+	free(copy);
 
-	free(dquoted_full_path);
+	run_win_executable(escaped, elevate);
+	free(escaped);
 #endif
 }
 
@@ -467,7 +465,7 @@ run_with_defaults(view_t *view)
 {
 	if(get_current_entry(view)->type == FT_DIR)
 	{
-		enter_dir(view);
+		(void)rn_enter_dir(view);
 		return;
 	}
 
@@ -555,7 +553,7 @@ rn_open_with(view_t *view, const char prog_spec[], int dont_execute,
 	}
 	else if(strcmp(prog_spec, VIFM_PSEUDO_CMD) == 0)
 	{
-		enter_dir(view);
+		(void)rn_enter_dir(view);
 	}
 	else if(strchr(prog_spec, '%') != NULL)
 	{
@@ -743,27 +741,29 @@ follow_link(view_t *view, int follow_dirs, int ultimate)
 	free(dir);
 }
 
-/* Handles opening of current entry of the view as a directory. */
-static void
-enter_dir(view_t *view)
+int
+rn_enter_dir(view_t *view)
 {
 	dir_entry_t *const curr = get_current_entry(view);
 
 	if(is_parent_dir(curr->name) && !curr->owns_origin)
 	{
 		rn_leave(view, 1);
-		return;
+		return 0;
 	}
 
 	char full_path[PATH_MAX + 1];
 	get_full_path_of(curr, sizeof(full_path), full_path);
-
-	if(cd_is_possible(full_path))
+	if(!cd_is_possible(full_path))
 	{
-		curr_stats.ch_pos = (cfg_ch_pos_on(CHPOS_ENTER) ? 1 : 0);
-		navigate_to(view, full_path);
-		curr_stats.ch_pos = 1;
+		return 1;
 	}
+
+	curr_stats.ch_pos = (cfg_ch_pos_on(CHPOS_ENTER) ? 1 : 0);
+	int result = navigate_to(view, full_path);
+	curr_stats.ch_pos = 1;
+
+	return result;
 }
 
 void
@@ -926,10 +926,7 @@ run_shell_finish(const char cmd[], const char final_cmd[], ShellPause pause,
 		stats_redraw_later();
 	}
 
-	if(curr_stats.load_stage > 0)
-	{
-		curs_set(0);
-	}
+	ui_set_cursor(/*visibility=*/0);
 
 	if(cmd == NULL)
 	{
@@ -970,7 +967,7 @@ setup_shellout_env(void)
 			return;
 	}
 
-	escaped_path = shell_like_escape(mount_file, 0);
+	escaped_path = shell_arg_escape(mount_file, curr_stats.shell_type);
 	cmd = format_str(term_multiplexer_fmt, FUSE_FILE_ENVVAR, escaped_path);
 	(void)vifm_system(cmd, SHELL_BY_APP);
 	free(cmd);
@@ -1042,7 +1039,7 @@ gen_term_multiplexer_cmd(const char cmd[], int pause, ShellRequester by)
 	title_arg = gen_term_multiplexer_title_arg(cmd);
 
 	raw_shell_cmd = format_str("%s%s", cmd, pause ? PAUSE_STR : "");
-	escaped_shell_cmd = shell_like_escape(raw_shell_cmd, 0);
+	escaped_shell_cmd = shell_arg_escape(raw_shell_cmd, curr_stats.shell_type);
 
 	const char *sh_flag = (by == SHELL_BY_USER ? cfg.shell_cmd_flag : "-c");
 
@@ -1050,7 +1047,7 @@ gen_term_multiplexer_cmd(const char cmd[], int pause, ShellRequester by)
 	{
 		char *const arg = format_str("%s %s %s", cfg.shell, sh_flag,
 				escaped_shell_cmd);
-		char *const escaped_arg = shell_like_escape(arg, 0);
+		char *const escaped_arg = shell_arg_escape(arg, curr_stats.shell_type);
 
 		shell_cmd = format_str("tmux new-window %s %s", title_arg, escaped_arg);
 
@@ -1115,7 +1112,7 @@ gen_term_multiplexer_title_arg(const char cmd[])
 	else
 	{
 		const char opt_c = (curr_stats.term_multiplexer == TM_SCREEN) ? 't' : 'n';
-		char *const escaped_title = shell_like_escape(title, 0);
+		char *const escaped_title = shell_arg_escape(title, curr_stats.shell_type);
 		title_arg = format_str("-%c %s", opt_c, escaped_title);
 		free(escaped_title);
 	}
@@ -1161,7 +1158,7 @@ gen_normal_cmd(const char cmd[], int pause)
 static void
 set_pwd_in_screen(const char path[])
 {
-	char *const escaped_dir = shell_like_escape(path, 0);
+	char *const escaped_dir = shell_arg_escape(path, curr_stats.shell_type);
 	char *const set_pwd = format_str("screen -X setenv PWD %s", escaped_dir);
 
 	(void)vifm_system(set_pwd, SHELL_BY_APP);
@@ -1293,7 +1290,7 @@ rn_ext(view_t *view, const char cmd[], const char title[], MacroFlags flags,
 	        ma_flags_present(flags, MF_CUSTOMVIEW_IOUTPUT) ||
 	        ma_flags_present(flags, MF_VERYCUSTOMVIEW_IOUTPUT))
 	{
-		rn_for_flist(view, cmd, title, flags);
+		rn_for_flist(view, cmd, title, /*user_sh=*/1, flags);
 	}
 	else
 	{
@@ -1333,6 +1330,9 @@ output_to_statusbar(const char cmd[], view_t *view, MacroFlags flags)
 
 	lines = NULL;
 	len = 0;
+	/* XXX: the loop can potentially never end if error pipe gets filled, might
+	 *      need to redirect stderr to /dev/null instead of opening and not using
+	 *      it. */
 	while(fgets(buf, sizeof(buf), file) == buf)
 	{
 		char *p;
@@ -1374,7 +1374,7 @@ run_in_split(const view_t *view, const char cmd[], int vert_split)
 {
 	char *const escaped_cmd = (cmd == NULL)
 	                        ? strdup(cfg.shell)
-	                        : shell_like_escape(cmd, 0);
+	                        : shell_arg_escape(cmd, curr_stats.shell_type);
 
 	setup_shellout_env();
 
@@ -1391,7 +1391,8 @@ run_in_split(const view_t *view, const char cmd[], int vert_split)
 
 		/* "eval" executes each argument as a separate argument, but escaping rules
 		 * are not exactly like in shell, so last command is run separately. */
-		char *const escaped_dir = shell_like_escape(flist_get_dir(view), 0);
+		char *const escaped_dir =
+			shell_arg_escape(flist_get_dir(view), curr_stats.shell_type);
 		if(vert_split)
 		{
 			snprintf(cmd, sizeof(cmd), "screen -X eval chdir\\ %s 'focus right' "
@@ -1440,7 +1441,7 @@ rn_start_bg_command(view_t *view, const char cmd[], MacroFlags flags)
 }
 
 int
-rn_for_flist(view_t *view, const char cmd[], const char title[],
+rn_for_flist(view_t *view, const char cmd[], const char title[], int user_sh,
 		MacroFlags flags)
 {
 	enum { MAX_TITLE_WIDTH = 80 };
@@ -1468,8 +1469,8 @@ rn_for_flist(view_t *view, const char cmd[], const char title[],
 	FILE *input_tmp = make_in_file(view, flags);
 
 	setup_shellout_env();
-	int error = (process_cmd_output("Loading custom view", cmd, input_tmp, 1,
-				interactive, &path_handler, view) != 0);
+	int error = (process_cmd_output("Loading custom view", cmd, input_tmp,
+				user_sh, interactive, &path_handler, view) != 0);
 	cleanup_shellout_env();
 
 	if(input_tmp != NULL)
diff --git a/src/running.h b/src/running.h
index 4f7894d..2b94069 100644
--- a/src/running.h
+++ b/src/running.h
@@ -46,6 +46,10 @@ struct view_t;
 /* Handles opening of current file/selection of the view. */
 void rn_open(struct view_t *view, FileHandleExec exec);
 
+/* Handles opening of current entry of the view as a directory.  Returns zero on
+ * success. */
+int rn_enter_dir(struct view_t *view);
+
 /* Follows file to find its true location (e.g. target of symbolic link) or just
  * opens it. */
 void rn_follow(struct view_t *view, int ultimate);
@@ -89,7 +93,7 @@ void rn_start_bg_command(struct view_t *view, const char cmd[],
  * or very custom view.  Returns zero on success, otherwise non-zero is
  * returned. */
 int rn_for_flist(struct view_t *view, const char cmd[], const char title[],
-		MacroFlags flags);
+		int user_sh, MacroFlags flags);
 
 /* Executes external command capturing its output as list of lines.  Sets *lines
  * and *nlines.  Returns zero on success, otherwise non-zero is returned. */
diff --git a/src/search.c b/src/search.c
index ccdc8c4..8d4c051 100644
--- a/src/search.c
+++ b/src/search.c
@@ -27,6 +27,8 @@
 
 #include "cfg/config.h"
 #include "compat/fs_limits.h"
+#include "engine/mode.h"
+#include "modes/modes.h"
 #include "ui/fileview.h"
 #include "ui/statusbar.h"
 #include "ui/ui.h"
@@ -36,38 +38,121 @@
 #include "utils/utils.h"
 #include "filelist.h"
 #include "flist_sel.h"
+#include "status.h"
 
-static int find_and_goto_match(view_t *view, int start, int backward);
-static void print_result(const view_t *view, int found, int backward);
+static int find_match(view_t *view, int start, int backward);
 
 int
-goto_search_match(view_t *view, int backward)
+search_find(view_t *view, const char pattern[], int backward,
+		int stash_selection, int select_matches, int count,
+		move_cursor_and_redraw_cb cb, int print_msg, int *found)
 {
-	const int wrap_start = backward ? view->list_rows : -1;
-	if(!find_and_goto_match(view, view->list_pos, backward))
+	int save_msg = 0;
+
+	if(search_pattern(view, pattern, stash_selection, select_matches) != 0)
 	{
-		if(!cfg.wrap_scan || !find_and_goto_match(view, wrap_start, backward))
+		*found = 0;
+		if(print_msg)
 		{
-			return 0;
+			print_search_fail_msg(view, backward);
+			save_msg = 1;
+			return save_msg;
 		}
+		/* If we're not printing messages, we might be interested in broken
+		 * pattern. */
+		return -1;
 	}
 
-	/* Redraw the cursor which also might synchronize cursors of two views. */
-	fview_cursor_redraw(view);
-	/* Schedule redraw of the view to highlight search matches. */
-	ui_view_schedule_redraw(view);
+	*found = goto_search_match(view, backward, count, cb);
+
+	if(print_msg)
+	{
+		save_msg = print_search_result(view, *found, backward, &print_search_msg);
+	}
+
+	return save_msg;
+}
+
+int
+search_next(view_t *view, int backward, int stash_selection, int select_matches,
+		int count, move_cursor_and_redraw_cb cb)
+{
+	int save_msg = 0;
+	int found;
+
+	if(hist_is_empty(&curr_stats.search_hist))
+	{
+		return save_msg;
+	}
+
+	if(view->matches == 0)
+	{
+		const char *const pattern = hists_search_last();
+		if(search_pattern(view, pattern, stash_selection, select_matches) != 0)
+		{
+			print_search_fail_msg(view, backward);
+			save_msg = 1;
+			return save_msg;
+		}
+	}
+
+	found = goto_search_match(view, backward, count, cb);
+
+	save_msg = print_search_result(view, found, backward, &print_search_next_msg);
+
+	return save_msg;
+}
+
+int
+goto_search_match(view_t *view, int backward, int count,
+		move_cursor_and_redraw_cb cb)
+{
+	const int i = find_search_match(view, backward, count);
+	if(i == -1)
+	{
+		return 0;
+	}
+
+	cb(i);
 
 	return 1;
 }
 
-/* Looks for a search match in specified direction from given start position and
- * navigates to it if match is found.  Starting position is not included in
- * searched range.  Returns non-zero if something was found, otherwise zero is
- * returned. */
+int
+find_search_match(view_t *view, int backward, int count)
+{
+	int c, i = view->list_pos;
+	assert(count > 0 && "Zero searches.");
+	for(c = 0; c < count; ++c)
+ 	{
+		i = find_match(view, i, backward);
+		if(i == -1)
+		{
+			if(cfg.wrap_scan)
+			{
+				const int wrap_start = backward ? view->list_rows : -1;
+				i = find_match(view, wrap_start, backward);
+				if(i == -1)
+				{
+					return -1;
+				}
+			}
+			else
+			{
+				return -1;
+			}
+		}
+ 	}
+	return i;
+}
+
+/* Looks for a search match in specified direction from given start position.
+ * Starting position is not included in searched range.  Returns index of a
+ * match, or -1 if no matches were found. */
 static int
-find_and_goto_match(view_t *view, int start, int backward)
+find_match(view_t *view, int start, int backward)
 {
-	int i;
+	int i = -1;
 	int begin, end, step;
 
 	if(backward)
@@ -80,6 +165,11 @@ find_and_goto_match(view_t *view, int start, int backward)
 	}
 	else
 	{
+		if(view->list_rows == 0)
+		{
+			return -1;
+		}
+
 		begin = start + 1;
 		end = view->list_rows;
 		step = 1;
@@ -91,25 +181,24 @@ find_and_goto_match(view_t *view, int start, int backward)
 	{
 		if(view->dir_entry[i].search_match)
 		{
-			view->list_pos = i;
-			break;
+			return i;
 		}
 	}
 
-	return i != end;
+	return -1;
 }
 
 int
-find_pattern(view_t *view, const char pattern[], int backward, int move,
-		int *found, int print_errors)
+search_pattern(view_t *view, const char pattern[], int stash_selection,
+		int select_matches)
 {
 	int cflags;
 	int nmatches = 0;
 	regex_t re;
-	int err;
+	int err = 0;
 	view_t *other;
 
-	if(move && cfg.hl_search)
+	if(stash_selection)
 	{
 		flist_sel_stash(view);
 	}
@@ -122,12 +211,9 @@ find_pattern(view_t *view, const char pattern[], int backward, int move,
 
 	if(pattern[0] == '\0')
 	{
-		*found = 1;
-		return 0;
+		return err;
 	}
 
-	*found = 0;
-
 	cflags = get_regexp_cflags(pattern);
 	if((err = regexp_compile(&re, pattern, cflags)) == 0)
 	{
@@ -161,7 +247,7 @@ find_pattern(view_t *view, const char pattern[], int backward, int move,
 			entry->match_left += escape_unreadableo(name, matches[0].rm_so);
 			entry->match_right = matches[0].rm_eo;
 			entry->match_right += escape_unreadableo(name, matches[0].rm_eo);
-			if(cfg.hl_search)
+			if(select_matches)
 			{
 				entry->selected = 1;
 				++view->selected_files;
@@ -174,12 +260,8 @@ find_pattern(view_t *view, const char pattern[], int backward, int move,
 	}
 	else
 	{
-		if(print_errors)
-		{
-			ui_sb_errf("Regexp error: %s", get_regexp_error(err, &re));
-		}
 		regfree(&re);
-		return -1;
+		return err;
 	}
 
 	other = (view == &lwin) ? &rwin : &lwin;
@@ -191,44 +273,36 @@ find_pattern(view_t *view, const char pattern[], int backward, int move,
 	view->matches = nmatches;
 	copy_str(view->last_search, sizeof(view->last_search), pattern);
 
-	view->matches = nmatches;
-	if(nmatches > 0)
-	{
-		const int was_found = move ? goto_search_match(view, backward) : 1;
-		*found = was_found;
+	return err;
+}
 
-		if(!cfg.hl_search)
+int
+print_search_result(const view_t *view, int found, int backward,
+		print_search_msg_cb cb)
+{
+	if(view->matches > 0)
+	{
+		/* Print a message in all cases except for 'hlsearch nowrapscan' with no
+		 * matches in non-visual mode to not supersede the "n files selected"
+		 * message for possibly hidden selected files (the message is printed
+		 * automatically). */
+		if(found)
 		{
-			if(print_errors)
-			{
-				print_result(view, was_found, backward);
-			}
+			cb(view, backward);
 			return 1;
 		}
-		return 0;
-	}
-	else
-	{
-		if(print_errors)
+		else if(!cfg.hl_search || cfg.wrap_scan || vle_mode_is(VISUAL_MODE) ||
+				vle_primary_mode_is(VISUAL_MODE))
 		{
 			print_search_fail_msg(view, backward);
+			return 1;
 		}
-		return 1;
-	}
-}
-
-/* Prints success or error message, determined by the found argument, about
- * search results to a user. */
-static void
-print_result(const view_t *view, int found, int backward)
-{
-	if(found)
-	{
-		print_search_msg(view, backward);
+		return 0;
 	}
 	else
 	{
 		print_search_fail_msg(view, backward);
+		return 1;
 	}
 }
 
diff --git a/src/search.h b/src/search.h
index 9b2d691..c510048 100644
--- a/src/search.h
+++ b/src/search.h
@@ -24,23 +24,59 @@ struct view_t;
 
 /* Search and navigation functions. */
 
-/* The move argument specifies whether cursor in the view should be adjusted to
- * point to just found file in case of successful search.  Sets *found to
- * non-zero if pattern was found, otherwise it's assigned zero.  print_errors
- * means user needs feedback, otherwise it can be provided later using one of
- * print_*_msg() functions.  Returns non-zero when a message was printed (or
- * would have been printed with print_errors set) to a user, otherwise zero is
- * returned.  Returned value is negative for incorrect pattern. */
-int find_pattern(struct view_t *view, const char pattern[], int backward,
-		int move, int *found, int print_errors);
-
-/* Looks for a search match in specified direction from current cursor position
- * taking search wrapping into account.  Returns non-zero if something was
- * found, otherwise zero is returned. */
-int goto_search_match(struct view_t *view, int backward);
+/* Callback for moving cursor to pos and redrawing both the cursor and a
+ * view. */
+typedef void (*move_cursor_and_redraw_cb)(int pos);
+
+/* Searches pattern in view, moves cursor to count's match using cb and
+ * optionally (if print_msg is set) shows a message.  stash_selection and
+ * select_matches are passed to search_pattern().  Sets *found to non-zero if
+ * pattern was found, otherwise it's assigned zero.  Returns non-zero when a
+ * message was printed to a user, otherwise zero is returned.  Returned value
+ * is negative for invalid pattern. */
+int search_find(struct view_t *view, const char pattern[], int backward,
+		int stash_selection, int select_matches, int count,
+		move_cursor_and_redraw_cb cb, int print_msg, int *found);
+
+/* Moves cursor to count's match using cb and shows a message.  Searches for
+ * the last pattern from the search history, if there are no matches in view.
+ * stash_selection and select_matches are passed to search_pattern().  Returns
+ * non-zero when a message was printed to a user, otherwise zero is returned. */
+int search_next(struct view_t *view, int backward, int stash_selection,
+		int select_matches, int count, move_cursor_and_redraw_cb cb);
+
+/* Searches pattern in view.  Does nothing in case pattern is empty.
+ * stash_selection stashes selection before the search.  select_matches selects
+ * found matches.  Returns non-zero for invalid pattern, otherwise zero is
+ * returned. */
+int search_pattern(struct view_t *view, const char pattern[],
+		int stash_selection, int select_matches);
+
+/* Looks for a count's search match in specified direction from current cursor
+ * position taking search wrapping into account.  Returns non-zero if something
+ * was found, otherwise zero is returned. */
+int goto_search_match(struct view_t *view, int backward, int count,
+		move_cursor_and_redraw_cb cb);
+
+/* Looks for a count's search match in specified direction from current cursor
+ * position taking search wrapping into account.  Returns index of a match, or
+ * -1 if no matches were found. */
+int find_search_match(struct view_t *view, int backward, int count);
 
 /* Auxiliary functions. */
 
+/* Callback for showing a message about search operation. */
+typedef void (*print_search_msg_cb)(const struct view_t *view,
+		int backward);
+
+/* Prints success or error message, determined by the found argument.  Supposed
+ * to be called after search_pattern() and the cursor positioned over a match.
+ * The success message is prtined by cb.  Takes search highlighting, wrapping
+ * and visual mode into account.  Returns non-zero if something was printed,
+ * otherwise zero is returned. */
+int print_search_result(const struct view_t *view, int found, int backward,
+		print_search_msg_cb cb);
+
 /* Prints results or error message about search operation to the user. */
 void print_search_msg(const struct view_t *view, int backward);
 
diff --git a/src/sort.c b/src/sort.c
index 7faaad3..792364a 100644
--- a/src/sort.c
+++ b/src/sort.c
@@ -228,8 +228,6 @@ sort_by_groups(dir_entry_t *entries, signed char key, size_t nentries)
 {
 	char **groups = NULL;
 	int ngroups = 0;
-	const int optimize = (view_sort_groups != view->sort_groups_g);
-	int i;
 
 	char *const copy = strdup(view_sort_groups);
 	char *group = copy, *state = NULL;
@@ -239,14 +237,19 @@ sort_by_groups(dir_entry_t *entries, signed char key, size_t nentries)
 	}
 	free(copy);
 
-	for(i = ngroups - (optimize ? 1 : 0); i >= 1; --i)
+	/* Whether view->primary_group can be used to skip compiling regexp of the
+	 * first group. */
+	const int optimized = (view_sort_groups == view->sort_groups);
+
+	int i;
+	for(i = ngroups - 1; i >= (optimized ? 1 : 0); --i)
 	{
 		regex_t regex;
 		(void)regexp_compile(&regex, groups[i], REG_EXTENDED | REG_ICASE);
 		sort_by_key(entries, nentries, key, &regex);
 		regfree(&regex);
 	}
-	if(optimize && ngroups != 0)
+	if(optimized && ngroups != 0)
 	{
 		sort_by_key(entries, nentries, key, &view->primary_group);
 	}
diff --git a/src/status.c b/src/status.c
index ebc83be..0bcfffe 100644
--- a/src/status.c
+++ b/src/status.c
@@ -212,7 +212,7 @@ load_def_values(status_t *stats, config_t *config)
 
 	stats->ellipsis = "...";
 
-	stats->shell_type = ST_NORMAL;
+	stats->shell_type = ST_POSIX;
 
 	stats->fuse_umount_cmd = "";
 
@@ -228,6 +228,10 @@ load_def_values(status_t *stats, config_t *config)
 
 	stats->preview_hint = NULL;
 
+	free_string_array(stats->plugins_dirs.items, stats->plugins_dirs.nitems);
+	stats->plugins_dirs.items = NULL;
+	stats->plugins_dirs.nitems = 0;
+
 	stats->global_local_settings = 0;
 
 	stats->history_size = 0;
@@ -533,7 +537,7 @@ hists_resize(int new_size)
 void
 hists_commands_save(const char command[])
 {
-	if(is_history_command(command))
+	if(cmds_goes_to_history(command))
 	{
 		if(!curr_stats.restart_in_progress && curr_stats.load_stage == 3)
 		{
diff --git a/src/status.h b/src/status.h
index 58b9929..9229799 100644
--- a/src/status.h
+++ b/src/status.h
@@ -27,6 +27,7 @@
 #include "compat/fs_limits.h"
 #include "ui/color_scheme.h"
 #include "utils/hist.h"
+#include "utils/string_array.h"
 #include "utils/test_helpers.h"
 #include "filetype.h"
 
@@ -91,8 +92,9 @@ TermState;
 
 typedef enum
 {
-	/* Shell that is aware of command escaping and backslashes in paths. */
-	ST_NORMAL,
+	/* POSIX-like shell that is aware of command escaping and backslashes in
+	 * paths. */
+	ST_POSIX,
 	/* Dumb cmd.exe shell on Windows. */
 	ST_CMD,
 	/* An improved version of cmd.exe shell on Windows. */
@@ -217,6 +219,9 @@ typedef struct
 
 	const void *preview_hint; /* Hint on which view is used for preview. */
 
+	/* List of plugin search directories.  Processed first to last. */
+	strlist_t plugins_dirs;
+
 	int global_local_settings; /* Set local settings globally. */
 
 	int history_size;    /* Number of elements in histories. */
diff --git a/src/tags.c b/src/tags.c
index 0698058..e873c4b 100644
--- a/src/tags.c
+++ b/src/tags.c
@@ -89,6 +89,8 @@ const char *tags[] = {
 	"vifm-'milleroptions'",
 	"vifm-'millerview'",
 	"vifm-'mintimeoutlen'",
+	"vifm-'mouse'",
+	"vifm-'navoptions'",
 	"vifm-'nu'",
 	"vifm-'number'",
 	"vifm-'numberwidth'",
@@ -129,10 +131,12 @@ const char *tags[] = {
 	"vifm-'syncregs'",
 	"vifm-'syscalls'",
 	"vifm-'tablabel'",
+	"vifm-'tabline'",
 	"vifm-'tabprefix'",
 	"vifm-'tabscope'",
 	"vifm-'tabstop'",
 	"vifm-'tabsuffix'",
+	"vifm-'tal'",
 	"vifm-'timefmt'",
 	"vifm-'timeoutlen'",
 	"vifm-'title'",
@@ -167,6 +171,7 @@ const char *tags[] = {
 	"vifm---logging",
 	"vifm---no-configs",
 	"vifm---on-choose",
+	"vifm---plugins-dir",
 	"vifm---remote",
 	"vifm---remote-expr",
 	"vifm---select",
@@ -184,8 +189,11 @@ const char *tags[] = {
 	"vifm-:!",
 	"vifm-:!!",
 	"vifm-:alink",
+	"vifm-:amap",
+	"vifm-:anoremap",
 	"vifm-:apropos",
 	"vifm-:au",
+	"vifm-:aunmap",
 	"vifm-:autocmd",
 	"vifm-:bar",
 	"vifm-:bmark",
@@ -342,6 +350,8 @@ const char *tags[] = {
 	"vifm-:redr",
 	"vifm-:redraw",
 	"vifm-:reg",
+	"vifm-:rege",
+	"vifm-:regedit",
 	"vifm-:registers",
 	"vifm-:regular",
 	"vifm-:rename",
@@ -505,6 +515,20 @@ const char *tags[] = {
 	"vifm-]s",
 	"vifm-]z",
 	"vifm-^",
+	"vifm-a_CTRL-J",
+	"vifm-a_CTRL-N",
+	"vifm-a_CTRL-O",
+	"vifm-a_CTRL-P",
+	"vifm-a_CTRL-Y",
+	"vifm-a_Down",
+	"vifm-a_End",
+	"vifm-a_Enter",
+	"vifm-a_Home",
+	"vifm-a_Left",
+	"vifm-a_PageDown",
+	"vifm-a_PageUp",
+	"vifm-a_Right",
+	"vifm-a_Up",
 	"vifm-al",
 	"vifm-app.txt",
 	"vifm-av",
@@ -545,6 +569,7 @@ const char *tags[] = {
 	"vifm-c_CTRL-X_m",
 	"vifm-c_CTRL-X_r",
 	"vifm-c_CTRL-X_t",
+	"vifm-c_CTRL-Y",
 	"vifm-c_CTRL-]",
 	"vifm-c_CTRL-_",
 	"vifm-c_Delete",
@@ -624,6 +649,7 @@ const char *tags[] = {
 	"vifm-extcached()",
 	"vifm-f",
 	"vifm-filename-modifiers",
+	"vifm-filereadable()",
 	"vifm-filetype()",
 	"vifm-filters",
 	"vifm-fnameescape()",
@@ -655,6 +681,7 @@ const char *tags[] = {
 	"vifm-h",
 	"vifm-has()",
 	"vifm-i",
+	"vifm-input()",
 	"vifm-j",
 	"vifm-jobcount-variable",
 	"vifm-k",
@@ -665,6 +692,7 @@ const char *tags[] = {
 	"vifm-l_VifmEntry.ctime",
 	"vifm-l_VifmEntry.folded",
 	"vifm-l_VifmEntry.gettarget()",
+	"vifm-l_VifmEntry.isdir",
 	"vifm-l_VifmEntry.location",
 	"vifm-l_VifmEntry.match",
 	"vifm-l_VifmEntry.matchend",
@@ -687,12 +715,15 @@ const char *tags[] = {
 	"vifm-l_VifmTab:getview()",
 	"vifm-l_VifmView",
 	"vifm-l_VifmView.currententry",
+	"vifm-l_VifmView.custom",
 	"vifm-l_VifmView.cwd",
 	"vifm-l_VifmView.entrycount",
 	"vifm-l_VifmView.locopts",
 	"vifm-l_VifmView.viewopts",
 	"vifm-l_VifmView:cd()",
 	"vifm-l_VifmView:entry()",
+	"vifm-l_VifmView:select()",
+	"vifm-l_VifmView:unselect()",
 	"vifm-l_vifm",
 	"vifm-l_vifm.addcolumntype()",
 	"vifm-l_vifm.addhandler()",
@@ -703,9 +734,13 @@ const char *tags[] = {
 	"vifm-l_vifm.currview()",
 	"vifm-l_vifm.errordialog()",
 	"vifm-l_vifm.escape()",
+	"vifm-l_vifm.events",
+	"vifm-l_vifm.events.listen()",
+	"vifm-l_vifm.executable()",
 	"vifm-l_vifm.exists()",
 	"vifm-l_vifm.expand()",
 	"vifm-l_vifm.fnamemodify()",
+	"vifm-l_vifm.input()",
 	"vifm-l_vifm.keys",
 	"vifm-l_vifm.keys.add()",
 	"vifm-l_vifm.makepath()",
@@ -744,6 +779,7 @@ const char *tags[] = {
 	"vifm-lua-api",
 	"vifm-lua-caps",
 	"vifm-lua-design",
+	"vifm-lua-events",
 	"vifm-lua-evolution",
 	"vifm-lua-handlers",
 	"vifm-lua-libs",
@@ -809,6 +845,8 @@ const char *tags[] = {
 	"vifm-mappings",
 	"vifm-menus-and-dialogs",
 	"vifm-more",
+	"vifm-mouse-overview",
+	"vifm-mouse-using",
 	"vifm-n",
 	"vifm-normal",
 	"vifm-options",
diff --git a/src/ui/color_scheme.c b/src/ui/color_scheme.c
index 6649786..58eed6d 100644
--- a/src/ui/color_scheme.c
+++ b/src/ui/color_scheme.c
@@ -86,6 +86,17 @@ char *HI_GROUPS[] = {
 	[USER7_COLOR]        = "User7",
 	[USER8_COLOR]        = "User8",
 	[USER9_COLOR]        = "User9",
+	[USER10_COLOR]       = "User10",
+	[USER11_COLOR]       = "User11",
+	[USER12_COLOR]       = "User12",
+	[USER13_COLOR]       = "User13",
+	[USER14_COLOR]       = "User14",
+	[USER15_COLOR]       = "User15",
+	[USER16_COLOR]       = "User16",
+	[USER17_COLOR]       = "User17",
+	[USER18_COLOR]       = "User18",
+	[USER19_COLOR]       = "User19",
+	[USER20_COLOR]       = "User20",
 	[OTHER_WIN_COLOR]    = "OtherWin",
 	[LINE_NUM_COLOR]     = "LineNr",
 	[ODD_LINE_COLOR]     = "OddLine",
@@ -129,6 +140,17 @@ const char *HI_GROUPS_DESCR[] = {
 	[USER7_COLOR]        = "user color #7",
 	[USER8_COLOR]        = "user color #8",
 	[USER9_COLOR]        = "user color #9",
+	[USER10_COLOR]       = "user color #10",
+	[USER11_COLOR]       = "user color #11",
+	[USER12_COLOR]       = "user color #12",
+	[USER13_COLOR]       = "user color #13",
+	[USER14_COLOR]       = "user color #14",
+	[USER15_COLOR]       = "user color #15",
+	[USER16_COLOR]       = "user color #16",
+	[USER17_COLOR]       = "user color #17",
+	[USER18_COLOR]       = "user color #18",
+	[USER19_COLOR]       = "user color #19",
+	[USER20_COLOR]       = "user color #20",
 	[OTHER_WIN_COLOR]    = "additional highlighting of inactive pane",
 	[LINE_NUM_COLOR]     = "color of line number column in panes",
 	[ODD_LINE_COLOR]     = "color of every second entry line in a pane",
@@ -488,6 +510,17 @@ static const col_attr_t default_cs[] = {
 	[USER7_COLOR]        = { -1,            -1,          -1                      },
 	[USER8_COLOR]        = { -1,            -1,          -1                      },
 	[USER9_COLOR]        = { -1,            -1,          -1                      },
+	[USER10_COLOR]       = { -1,            -1,          -1                      },
+	[USER11_COLOR]       = { -1,            -1,          -1                      },
+	[USER12_COLOR]       = { -1,            -1,          -1                      },
+	[USER13_COLOR]       = { -1,            -1,          -1                      },
+	[USER14_COLOR]       = { -1,            -1,          -1                      },
+	[USER15_COLOR]       = { -1,            -1,          -1                      },
+	[USER16_COLOR]       = { -1,            -1,          -1                      },
+	[USER17_COLOR]       = { -1,            -1,          -1                      },
+	[USER18_COLOR]       = { -1,            -1,          -1                      },
+	[USER19_COLOR]       = { -1,            -1,          -1                      },
+	[USER20_COLOR]       = { -1,            -1,          -1                      },
 	[OTHER_WIN_COLOR]    = { -1,            -1,          -1                      },
 	[LINE_NUM_COLOR]     = { -1,            -1,          -1                      },
 	[ODD_LINE_COLOR]     = { -1,            -1,          -1                      },
@@ -502,7 +535,7 @@ static file_hi_t * clone_cs_highlights(const col_scheme_t *from);
 static void reset_cs_colors(col_scheme_t *cs);
 static int source_cs(const char name[]);
 static void get_cs_path(const char name[], char buf[], size_t buf_size);
-static const char * get_global_colors_dir(void);
+static int get_colors_dir(int idx, char buf[], size_t buf_len);
 static void check_cs(col_scheme_t *cs);
 static void load_color_pairs(col_scheme_t *cs);
 static void ensure_dir_map_exists(void);
@@ -617,8 +650,17 @@ list_cs_files(int *len)
 	char **list = NULL;
 	*len = 0;
 
-	list = list_regular_files(cfg.colors_dir, list, len);
-	list = list_regular_files(get_global_colors_dir(), list, len);
+	int i = 0;
+	while(1)
+	{
+		char colors_dir[PATH_MAX + 1];
+		if(!get_colors_dir(i++, colors_dir, sizeof(colors_dir)))
+		{
+			break;
+		}
+
+		list = list_regular_files(colors_dir, list, len);
+	}
 
 	return list;
 }
@@ -1014,39 +1056,47 @@ source_cs(const char name[])
 static void
 get_cs_path(const char name[], char buf[], size_t buf_size)
 {
-	snprintf(buf, buf_size, "%s/%s.vifm", cfg.colors_dir, name);
-	if(is_regular_file(buf))
+	int i = 0;
+	while(1)
 	{
-		return;
-	}
+		char colors_dir[PATH_MAX + 1];
+		if(!get_colors_dir(i++, colors_dir, sizeof(colors_dir)))
+		{
+			break;
+		}
 
-	(void)cut_suffix(buf, ".vifm");
-	if(is_regular_file(buf))
-	{
-		return;
-	}
+		snprintf(buf, buf_size, "%s/%s.vifm", colors_dir, name);
+		if(is_regular_file(buf))
+		{
+			break;
+		}
 
-	snprintf(buf, buf_size, "%s/%s.vifm", get_global_colors_dir(),
-			name);
-	if(is_regular_file(buf))
-	{
-		return;
+		(void)cut_suffix(buf, ".vifm");
+		if(is_regular_file(buf))
+		{
+			break;
+		}
 	}
-
-	(void)cut_suffix(buf, ".vifm");
 }
 
-/* Retrieves path to global directory containing color schemes.  Returns the
- * path. */
-static const char *
-get_global_colors_dir(void)
+/* Retrieves path to a directory containing color schemes.  Returns non-zero on
+ * success. */
+static int
+get_colors_dir(int idx, char buf[], size_t buf_len)
 {
-	static char dir_path[PATH_MAX + 1];
-	if(dir_path[0] == '\0')
+	if(idx == 0)
 	{
-		snprintf(dir_path, sizeof(dir_path), "%s/colors", get_sys_conf_dir());
+		return (copy_str(buf, buf_len, cfg.colors_dir) < buf_len);
 	}
-	return dir_path;
+
+	const char *conf_dir = get_sys_conf_dir(idx - 1);
+	if(conf_dir == NULL)
+	{
+		return 0;
+	}
+
+	int written = snprintf(buf, buf_len, "%s/colors", conf_dir);
+	return (written >= 0 && written < (int)buf_len);
 }
 
 /* Checks whether colorscheme is in unusable state and resets it to normal
diff --git a/src/ui/colored_line.c b/src/ui/colored_line.c
index d5ed301..24e18e4 100644
--- a/src/ui/colored_line.c
+++ b/src/ui/colored_line.c
@@ -28,6 +28,9 @@
 #include "color_scheme.h"
 #include "ui.h"
 
+/* First character used for an attribute. */
+#define FIRST_ATTR 'a'
+
 static size_t effective_chrw(const char line[]);
 
 cline_t
@@ -53,10 +56,13 @@ cline_sync(cline_t *cline, int extra_width)
 }
 
 void
-cline_set_attr(cline_t *cline, char attr)
+cline_set_attr(cline_t *cline, int user_color)
 {
 	(void)cline_sync(cline, 1);
-	cline->attrs[cline->attrs_len - 1] = attr;
+	if(user_color >= 0 && user_color <= LAST_USER_COLOR)
+	{
+		cline->attrs[cline->attrs_len - 1] = FIRST_ATTR + user_color;
+	}
 }
 
 void
@@ -77,6 +83,14 @@ cline_finish(cline_t *cline)
 	}
 }
 
+void
+cline_append(cline_t *cline, cline_t *admixture)
+{
+	cline_splice_attrs(cline, admixture);
+	strappend(&cline->line, &cline->line_len, admixture->line);
+	free(admixture->line);
+}
+
 void
 cline_splice_attrs(cline_t *cline, cline_t *admixture)
 {
@@ -104,13 +118,13 @@ cline_print(const cline_t *cline, WINDOW *win, const col_attr_t *def_col)
 	cchar_t attr = def_attr;
 	while(*line != '\0')
 	{
-		if(*attrs == '0')
+		if(*attrs == FIRST_ATTR)
 		{
 			attr = def_attr;
 		}
 		else if(*attrs != ' ')
 		{
-			const int color = (USER1_COLOR + (*attrs - '1'));
+			const int color = USER1_COLOR + (*attrs - (FIRST_ATTR + 1));
 			col_attr_t col = *def_col;
 			cs_mix_colors(&col, &cfg.cs.color[color]);
 			attr = cs_color_to_cchar(&col, -1);
diff --git a/src/ui/colored_line.h b/src/ui/colored_line.h
index 9fa053e..c90c786 100644
--- a/src/ui/colored_line.h
+++ b/src/ui/colored_line.h
@@ -25,7 +25,7 @@
 
 /* This unit provides a type that bundles line with attributes.  Attributes are
  * stored in a parallel array of characters.  It contains a character in the
- * set [0-9 ] (space included) per screen position of the UTF-8 line.  Each
+ * set [a-u ] (space included) per screen position of the UTF-8 line.  Each
  * attribute character specifies which user highlight group should be used
  * starting with that offset on the screen. */
 
@@ -47,8 +47,9 @@ cline_t cline_make(void);
  * non-zero if cline->attrs has extra characters compared to cline->line. */
 int cline_sync(cline_t *cline, int extra_width);
 
-/* Sets attribute to be used for text appended later on. */
-void cline_set_attr(cline_t *cline, char attr);
+/* Sets user group attribute to be used for text appended later on.  Zero means
+ * no group.  Out of range values are not applied. */
+void cline_set_attr(cline_t *cline, int user_color);
 
 /* Makes cline empty. */
 void cline_clear(cline_t *cline);
@@ -57,6 +58,9 @@ void cline_clear(cline_t *cline);
  * attributes match each other. */
 void cline_finish(cline_t *cline);
 
+/* Appends line and attributes from admixture freeing them afterwards. */
+void cline_append(cline_t *cline, cline_t *admixture);
+
 /* Appends attributes from admixture freeing them (but not the line). */
 void cline_splice_attrs(cline_t *cline, cline_t *admixture);
 
diff --git a/src/ui/colors.h b/src/ui/colors.h
index 5cc8963..33e170a 100644
--- a/src/ui/colors.h
+++ b/src/ui/colors.h
@@ -81,12 +81,26 @@ enum
 	USER7_COLOR,        /* User color #7. */
 	USER8_COLOR,        /* User color #8. */
 	USER9_COLOR,        /* User color #9. */
+	USER10_COLOR,       /* User color #10. */
+	USER11_COLOR,       /* User color #11. */
+	USER12_COLOR,       /* User color #12. */
+	USER13_COLOR,       /* User color #13. */
+	USER14_COLOR,       /* User color #14. */
+	USER15_COLOR,       /* User color #15. */
+	USER16_COLOR,       /* User color #16. */
+	USER17_COLOR,       /* User color #17. */
+	USER18_COLOR,       /* User color #18. */
+	USER19_COLOR,       /* User color #19. */
+	USER20_COLOR,       /* User color #20. */
 	OTHER_WIN_COLOR,    /* Background and highlighting for inactive pane. */
 	LINE_NUM_COLOR,     /* Color of line number column of panes. */
 	ODD_LINE_COLOR,     /* Color of every second entry line in a pane. */
 	MAXNUM_COLOR        /* Number of elements of a color scheme. */
 };
 
+/* The last of USER*_COLOR groups. */
+#define LAST_USER_COLOR 20
+
 #endif /* VIFM__UI__COLORS_H__ */
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/src/ui/fileview.c b/src/ui/fileview.c
index 1ab5576..7a4b9f1 100644
--- a/src/ui/fileview.c
+++ b/src/ui/fileview.c
@@ -60,8 +60,9 @@
 
 static void draw_left_column(view_t *view);
 static void draw_right_column(view_t *view);
-static void print_column(view_t *view, entries_t entries, const char current[],
-		const char path[], int width, int offset, int number_width);
+static void print_side_column(view_t *view, entries_t entries,
+		const char current[], const char path[], int width, int offset,
+		int number_width);
 static void fill_column(view_t *view, int start_line, int top, int width,
 		int offset);
 static void calculate_table_conf(view_t *view, size_t *count, size_t *width);
@@ -70,10 +71,7 @@ static int calculate_number_width(const view_t *view, int list_length,
 static int count_digits(int num);
 static int calculate_top_position(view_t *view, int top);
 static int get_line_color(const view_t *view, const dir_entry_t *entry);
-static size_t calculate_print_width(const view_t *view, int i,
-		size_t max_width);
-static void draw_cell(columns_t *columns, column_data_t *cdt, size_t col_width,
-		size_t print_width);
+static void draw_cell(columns_t *columns, column_data_t *cdt, size_t col_width);
 static columns_t * get_view_columns(const view_t *view, int truncated);
 static columns_t * get_name_column(int truncated);
 static void consider_scroll_bind(view_t *view);
@@ -81,7 +79,7 @@ static cchar_t prepare_inactive_color(view_t *view, dir_entry_t *entry,
 		int line_color);
 static void redraw_cell(view_t *view, int top, int cursor, int is_current);
 static void compute_and_draw_cell(column_data_t *cdt, int cell,
-		size_t col_width);
+		size_t col_count, size_t col_width);
 static void column_line_print(const char buf[], size_t offset, AlignType align,
 		const char full_column[], const format_info_t *info);
 static void draw_line_number(const column_data_t *cdt, int column);
@@ -136,6 +134,7 @@ static void format_id(void *data, size_t buf_len, char buf[],
 		const format_info_t *info);
 static size_t calculate_column_width(view_t *view);
 static size_t calculate_columns_count(view_t *view);
+static int has_extra_tls_col(const view_t *view, int col_width);
 static preview_area_t get_miller_preview_area(view_t *view);
 static size_t get_max_filename_width(const view_t *view);
 static size_t get_filename_width(const view_t *view, int i);
@@ -229,6 +228,7 @@ fview_reset(view_t *view)
 {
 	view->ls_view_g = view->ls_view = 0;
 	view->ls_transposed_g = view->ls_transposed = 0;
+	view->ls_cols_g = view->ls_cols = 0;
 	/* Invalidate maximum file name widths cache. */
 	view->max_filename_width = 0;;
 	view->column_count = 1;
@@ -238,7 +238,7 @@ fview_reset(view_t *view)
 	view->miller_ratios_g[0] = view->miller_ratios[0] = 1;
 	view->miller_ratios_g[1] = view->miller_ratios[1] = 1;
 	view->miller_ratios_g[2] = view->miller_ratios[2] = 1;
-	view->miller_preview_files_g = view->miller_preview_files = 0;
+	view->miller_preview_g = view->miller_preview = MP_DIRS;
 
 	view->num_type_g = view->num_type = NT_NONE;
 	view->num_width_g = view->num_width = 4;
@@ -298,10 +298,8 @@ draw_dir_list_only(view_t *view)
 	draw_left_column(view);
 
 	visible_cells = view->window_cells;
-	if(fview_is_transposed(view) &&
-			view->column_count*(int)col_width < ui_view_available_width(view))
+	if(has_extra_tls_col(view, col_width))
 	{
-		/* Add extra visual column to display more context. */
 		visible_cells += view->window_rows;
 	}
 
@@ -316,7 +314,7 @@ draw_dir_list_only(view_t *view)
 			.current_pos = view->list_pos,
 		};
 
-		compute_and_draw_cell(&cdt, cell, col_width);
+		compute_and_draw_cell(&cdt, cell, col_count, col_width);
 	}
 
 	draw_right_column(view);
@@ -346,7 +344,7 @@ draw_left_column(view_t *view)
 	               - (cfg.extra_padding ? 1 : 0) - 1;
 	if(lcol_width <= 0)
 	{
-		flist_free_cache(view, &view->left_column);
+		flist_free_cache(&view->left_column);
 		return;
 	}
 
@@ -360,7 +358,7 @@ draw_left_column(view_t *view)
 
 	if(view->left_column.entries.nentries >= 0)
 	{
-		print_column(view, view->left_column.entries, dir, path, lcol_width, 0,
+		print_side_column(view, view->left_column.entries, dir, path, lcol_width, 0,
 				number_width);
 	}
 }
@@ -380,7 +378,7 @@ draw_right_column(view_t *view)
 	const int rcol_width = ui_view_right_reserved(view) - padding - 1;
 	if(rcol_width <= 0)
 	{
-		flist_free_cache(view, &view->right_column);
+		flist_free_cache(&view->right_column);
 		return;
 	}
 
@@ -399,13 +397,18 @@ draw_right_column(view_t *view)
 	};
 
 	dir_entry_t *const entry = get_current_entry(view);
-	if(view->miller_preview_files && !fentry_is_dir(entry))
+	if(view->miller_preview != MP_DIRS && !fentry_is_dir(entry))
 	{
 		const char *clear_cmd = qv_draw_on(entry, &parea);
 		update_string(&view->file_preview_clear_cmd, clear_cmd);
 		return;
 	}
 
+	if(view->miller_preview == MP_FILES && fentry_is_dir(entry))
+	{
+		return;
+	}
+
 	if(displayed_graphics)
 	{
 		/* Do this even if there is no clear command. */
@@ -419,7 +422,7 @@ draw_right_column(view_t *view)
 
 	if(view->right_column.entries.nentries >= 0)
 	{
-		print_column(view, view->right_column.entries, NULL, path, rcol_width,
+		print_side_column(view, view->right_column.entries, NULL, path, rcol_width,
 				offset, 0);
 	}
 }
@@ -427,7 +430,7 @@ draw_right_column(view_t *view)
 /* Prints column full of entry names.  Current is a hint that tells which column
  * has to be selected (otherwise position from history record is used). */
 static void
-print_column(view_t *view, entries_t entries, const char current[],
+print_side_column(view_t *view, entries_t entries, const char current[],
 		const char path[], int width, int offset, int number_width)
 {
 	columns_t *const columns = get_name_column(0);
@@ -477,6 +480,8 @@ print_column(view_t *view, entries_t entries, const char current[],
 		flist_hist_update(view, path, get_last_path_component(current), pos - top);
 	}
 
+	int padding = (cfg.extra_padding ? 1 : 0);
+
 	for(i = top; i < entries.nentries && i - top < view->window_rows; ++i)
 	{
 		size_t prefix_len = 0U;
@@ -493,7 +498,7 @@ print_column(view_t *view, entries_t entries, const char current[],
 			.prefix_len = &prefix_len,
 		};
 
-		draw_cell(columns, &cdt, width, width - 1);
+		draw_cell(columns, &cdt, width - padding);
 	}
 
 	fill_column(view, i, top, number_width + width, offset);
@@ -665,28 +670,28 @@ get_line_color(const view_t *view, const dir_entry_t *entry)
 	}
 }
 
-/* Calculates width of the column using entry and maximum width. */
-static size_t
-calculate_print_width(const view_t *view, int i, size_t max_width)
+/* Draws a full cell of the file list.  col_width doesn't include extra padding!
+ * The total printed widths will be that plus two empty cells (one before and
+ * one after). */
+static void
+draw_cell(columns_t *columns, column_data_t *cdt, size_t col_width)
 {
-	if(!ui_view_displays_columns(view))
+	size_t width_left;
+	if(cdt->view->ls_view)
 	{
-		const size_t raw_name_width = get_filename_width(view, i);
-		return MIN(max_width - 1, raw_name_width);
+		width_left = cdt->view->window_cols
+		           - cdt->column_offset
+		           - ui_view_right_reserved(cdt->view)
+		           - (cfg.extra_padding ? 2 : 0);
+	}
+	else
+	{
+		width_left = cdt->is_main
+		           ? ui_view_available_width(cdt->view) -
+		             (cdt->column_offset - ui_view_left_reserved(cdt->view))
+		           : col_width + 1U;
 	}
 
-	return max_width;
-}
-
-/* Draws a full cell of the file list.  print_width <= col_width. */
-static void
-draw_cell(columns_t *columns, column_data_t *cdt, size_t col_width,
-		size_t print_width)
-{
-	size_t width_left = cdt->is_main
-	                  ? ui_view_available_width(cdt->view) - (cdt->column_offset -
-	                    ui_view_left_reserved(cdt->view))
-	                  : col_width + 1U;
 	const format_info_t info = {
 		.data = cdt,
 		.id = FILL_COLUMN_ID
@@ -699,9 +704,9 @@ draw_cell(columns_t *columns, column_data_t *cdt, size_t col_width,
 
 	columns_format_line(columns, cdt, MIN(col_width, width_left));
 
-	if(cfg.extra_padding && width_left >= col_width)
+	if(cfg.extra_padding)
 	{
-		column_line_print(" ", print_width, AT_LEFT, " ", &info);
+		column_line_print(" ", col_width, AT_LEFT, " ", &info);
 	}
 }
 
@@ -797,11 +802,11 @@ consider_scroll_bind(view_t *view)
 		other->top_line *= other->column_count;
 		other->top_line = calculate_top_position(other, other->top_line);
 
-		if(can_scroll_up(other))
+		if(fpos_can_scroll_back(other))
 		{
 			(void)fpos_scroll_down(other, 0);
 		}
-		if(can_scroll_down(other))
+		if(fpos_can_scroll_fwd(other))
 		{
 			(void)fpos_scroll_up(other, 0);
 		}
@@ -869,6 +874,23 @@ fview_cursor_redraw(view_t *view)
 	}
 }
 
+void
+fview_clear_miller_preview(view_t *view)
+{
+	if(!view->miller_view || view->miller_preview == MP_DIRS)
+	{
+		return;
+	}
+
+	const int padding = (cfg.extra_padding ? 1 : 0);
+	const int rcol_width = ui_view_right_reserved(view) - padding - 1;
+	if(rcol_width > 0)
+	{
+		const preview_area_t parea = get_miller_preview_area(view);
+		qv_cleanup_area(&parea, view->file_preview_clear_cmd);
+	}
+}
+
 void
 fview_draw_inactive_cursor(view_t *view)
 {
@@ -953,22 +975,21 @@ redraw_cell(view_t *view, int top, int cursor, int is_current)
 		.line_pos = pos,
 		.current_pos = is_current ? view->list_pos : -1,
 	};
-	compute_and_draw_cell(&cdt, cursor, col_width);
+	compute_and_draw_cell(&cdt, cursor, col_count, col_width);
 }
 
 /* Fills in fields of cdt based on passed in arguments and
  * view/entry/line_pos/current_pos fields of cdt.  Then draws the cell. */
 static void
-compute_and_draw_cell(column_data_t *cdt, int cell, size_t col_width)
+compute_and_draw_cell(column_data_t *cdt, int cell, size_t col_count,
+		size_t col_width)
 {
 	size_t prefix_len = 0U;
 
-	const size_t print_width = calculate_print_width(cdt->view, cdt->line_pos,
-			col_width);
+	int col = fpos_get_col(cdt->view, cell);
 
 	cdt->current_line = fpos_get_line(cdt->view, cell);
-	cdt->column_offset = ui_view_left_reserved(cdt->view)
-	                   + fpos_get_col(cdt->view, cell)*col_width;
+	cdt->column_offset = ui_view_left_reserved(cdt->view) + col*col_width;
 	cdt->line_hi_group = get_line_color(cdt->view, cdt->entry);
 	cdt->number_width = cdt->view->real_num_width;
 	cdt->total_width = ui_view_available_width(cdt->view);
@@ -977,31 +998,28 @@ compute_and_draw_cell(column_data_t *cdt, int cell, size_t col_width)
 
 	if(cfg.extra_padding && !ui_view_displays_columns(cdt->view))
 	{
-		/* Padding in ls-like view adds additional empty single character between
-		 * columns, on which we shouldn't draw anything here. */
-		--col_width;
+		if(cdt->view->ls_cols != 0 && col_count > 1 && col == (int)col_count - 1)
+		{
+			/* Reserve one character column after the last ls column. */
+			col_width -= 1;
+		}
+		else
+		{
+			/* Reserve two character columns between two ls columns to draw padding or
+			 * before and after single column if it's the only one. */
+			col_width -= 2;
+		}
 	}
 
-	draw_cell(get_view_columns(cdt->view, cell >= cdt->view->window_cells), cdt,
-			col_width, print_width);
+	int truncated = (cell >= cdt->view->window_cells);
+	columns_t *columns = get_view_columns(cdt->view, truncated);
+	draw_cell(columns, cdt, col_width);
 
 	cdt->prefix_len = NULL;
 }
 
-int
-can_scroll_up(const view_t *view)
-{
-	return view->top_line > 0;
-}
-
-int
-can_scroll_down(const view_t *view)
-{
-	return fpos_get_last_visible_cell(view) < view->list_rows - 1;
-}
-
 void
-scroll_up(view_t *view, int by)
+fview_scroll_back_by(view_t *view, int by)
 {
 	/* Round it up, so 1 will cause one line scrolling. */
 	view->top_line -= view->run_size*DIV_ROUND_UP(by, view->run_size);
@@ -1015,7 +1033,7 @@ scroll_up(view_t *view, int by)
 }
 
 void
-scroll_down(view_t *view, int by)
+fview_scroll_fwd_by(view_t *view, int by)
 {
 	/* Round it up, so 1 will cause one line scrolling. */
 	view->top_line += view->run_size*DIV_ROUND_UP(by, view->run_size);
@@ -1024,36 +1042,21 @@ scroll_down(view_t *view, int by)
 	view->curr_line = view->list_pos - view->top_line;
 }
 
-int
-get_corrected_list_pos_down(const view_t *view, int pos_delta)
+void
+fview_scroll_by(view_t *view, int by)
 {
-	const int scroll_offset = fpos_get_offset(view);
-	if(view->list_pos <= view->top_line + scroll_offset + (MAX(pos_delta, 1) - 1))
+	if(by > 0)
 	{
-		const int column_correction = view->list_pos%view->column_count;
-		const int offset = scroll_offset + pos_delta + column_correction;
-		return view->top_line + offset;
+		fview_scroll_fwd_by(view, by);
 	}
-	return view->list_pos;
-}
-
-int
-get_corrected_list_pos_up(const view_t *view, int pos_delta)
-{
-	const int scroll_offset = fpos_get_offset(view);
-	const int last = fpos_get_last_visible_cell(view);
-	if(view->list_pos >= last - scroll_offset - (MAX(pos_delta, 1) - 1))
+	else if(by < 0)
 	{
-		const int column_correction = (view->column_count - 1)
-		                            - view->list_pos%view->column_count;
-		const int offset = scroll_offset + pos_delta + column_correction;
-		return last - offset;
+		fview_scroll_back_by(view, -by);
 	}
-	return view->list_pos;
 }
 
 int
-consider_scroll_offset(view_t *view)
+fview_enforce_scroll_offset(view_t *view)
 {
 	int need_redraw = 0;
 	int pos = view->list_pos;
@@ -1061,18 +1064,18 @@ consider_scroll_offset(view_t *view)
 	{
 		const int s = fpos_get_offset(view);
 		/* Check scroll offset at the top. */
-		if(can_scroll_up(view) && pos - view->top_line < s)
+		if(fpos_can_scroll_back(view) && pos - view->top_line < s)
 		{
-			scroll_up(view, s - (pos - view->top_line));
+			fview_scroll_back_by(view, s - (pos - view->top_line));
 			need_redraw = 1;
 		}
 		/* Check scroll offset at the bottom. */
-		if(can_scroll_down(view))
+		if(fpos_can_scroll_fwd(view))
 		{
 			const int last = fpos_get_last_visible_cell(view);
 			if(pos > last - s)
 			{
-				scroll_down(view, s + (pos - last));
+				fview_scroll_fwd_by(view, s + (pos - last));
 				need_redraw = 1;
 			}
 		}
@@ -1081,24 +1084,21 @@ consider_scroll_offset(view_t *view)
 }
 
 void
-scroll_by_files(view_t *view, int by)
+fview_scroll_page_up(view_t *view)
 {
-	if(by > 0)
-	{
-		scroll_down(view, by);
-	}
-	else if(by < 0)
+	if(fpos_can_scroll_back(view))
 	{
-		scroll_up(view, -by);
+		fpos_scroll_page(view, fpos_get_last_visible_cell(view), -1);
 	}
 }
 
 void
-update_scroll_bind_offset(void)
+fview_scroll_page_down(view_t *view)
 {
-	const int rwin_pos = rwin.top_line/rwin.column_count;
-	const int lwin_pos = lwin.top_line/lwin.column_count;
-	curr_stats.scroll_bind_off = rwin_pos - lwin_pos;
+	if(fpos_can_scroll_fwd(view))
+	{
+		fpos_scroll_page(view, view->top_line, 1);
+	}
 }
 
 /* Print callback for column_view unit. */
@@ -1122,6 +1122,8 @@ column_line_print(const char buf[], size_t offset, AlignType align,
 	                 || info->id == SK_BY_INAME
 	                 || info->id == SK_BY_ROOT
 	                 || info->id == SK_BY_FILEROOT
+	                 || info->id == SK_BY_EXTENSION
+	                 || info->id == SK_BY_FILEEXT
 	                 || vlua_viewcolumn_is_primary(curr_stats.vlua, info->id);
 	const cchar_t line_attrs = prepare_col_color(view, primary, 0, cdt);
 
@@ -1763,7 +1765,7 @@ fview_is_transposed(const view_t *view)
 int
 fview_previews(view_t *view, const char path[])
 {
-	if(!view->miller_view || !view->miller_preview_files)
+	if(!view->miller_view || view->miller_preview == MP_DIRS)
 	{
 		return 0;
 	}
@@ -1791,13 +1793,86 @@ fview_set_millerview(view_t *view, int enabled)
 static size_t
 calculate_column_width(view_t *view)
 {
-	const int column_gap = (cfg.extra_padding ? 2 : 1);
-	if(view->max_filename_width == 0)
+	size_t max_width = view->window_cols
+	                 - ui_view_left_reserved(view) - ui_view_right_reserved(view);
+
+	size_t column_width;
+	if(view->ls_cols == 0)
+	{
+		if(view->max_filename_width == 0)
+		{
+			view->max_filename_width = get_max_filename_width(view);
+		}
+
+		const int column_gap = (cfg.extra_padding ? 2 : 1);
+		column_width = view->max_filename_width + column_gap;
+	}
+	else
+	{
+		column_width = max_width/view->ls_cols;
+	}
+
+	return MIN(column_width, max_width);
+}
+
+int
+fview_map_coordinates(view_t *view, int x, int y)
+{
+	if(view->miller_view)
+	{
+		const int padding = (cfg.extra_padding ? 1 : 0);
+		const int lcol_end = ui_view_left_reserved(view);
+		const int rcol_start = lcol_end + padding
+		                     + ui_view_available_width(view) + padding;
+
+		if(x < lcol_end)
+		{
+			return FVM_LEAVE;
+		}
+		if(x >= rcol_start)
+		{
+			return FVM_OPEN;
+		}
+	}
+
+	int pos;
+	if(ui_view_displays_columns(view))
+	{
+	  pos = view->top_line + y;
+	}
+	else
 	{
-		view->max_filename_width = get_max_filename_width(view);
+		size_t col_count, col_width;
+		calculate_table_conf(view, &col_count, &col_width);
+
+		if(has_extra_tls_col(view, col_width))
+		{
+			++col_count;
+		}
+
+		size_t x_offset = x/col_width;
+		if(x_offset >= col_count)
+		{
+			return FVM_NONE;
+		}
+
+		pos = fview_is_transposed(view)
+		    ? view->top_line + view->run_size*x_offset + y
+		    : view->top_line + view->run_size*y + x_offset;
 	}
-	return MIN(view->max_filename_width + column_gap,
-	           (size_t)ui_view_available_width(view));
+
+	return (pos < view->list_rows ? pos : FVM_NONE);
+}
+
+/* Whether there is an extra visual column to transposed ls-like view to display
+ * more context (when available and unless user requested fixed number of
+ * columns).  Returns non-zero if so. */
+static int
+has_extra_tls_col(const view_t *view, int col_width)
+{
+	return fview_is_transposed(view)
+			&& view->ls_cols == 0
+			&& view->column_count*col_width < ui_view_available_width(view);
 }
 
 void
@@ -1813,17 +1888,7 @@ void
 fview_dir_updated(view_t *view)
 {
 	view->local_cs = cs_load_local(view == &lwin, view->curr_dir);
-
-	if(view->miller_view && view->miller_preview_files)
-	{
-		const int padding = (cfg.extra_padding ? 1 : 0);
-		const int rcol_width = ui_view_right_reserved(view) - padding - 1;
-		if(rcol_width > 0)
-		{
-			const preview_area_t parea = get_miller_preview_area(view);
-			qv_cleanup_area(&parea, view->file_preview_clear_cmd);
-		}
-	}
+	fview_clear_miller_preview(view);
 }
 
 /* Computes area description for miller preview.  Returns the area. */
@@ -1874,7 +1939,9 @@ calculate_columns_count(view_t *view)
 	if(!ui_view_displays_columns(view))
 	{
 		const size_t column_width = calculate_column_width(view);
-		return ui_view_available_width(view)/column_width;
+		size_t max_width = view->window_cols - ui_view_left_reserved(view)
+		                 - ui_view_right_reserved(view);
+		return max_width/column_width;
 	}
 	return 1U;
 }
@@ -2055,10 +2122,12 @@ position_hardware_cursor(view_t *view)
 		return;
 	}
 
+	int cell = view->list_pos - view->top_line;
+
 	calculate_table_conf(view, &col_count, &col_width);
-	current_line = view->curr_line/col_count;
+	current_line = fpos_get_line(view, cell);
 	column_offset = ui_view_left_reserved(view)
-	              + (view->curr_line%col_count)*col_width;
+	              + fpos_get_col(view, cell)*col_width;
 	format_name(NULL, sizeof(buf) - 1U, buf, &info);
 
 	checked_wmove(view->win, current_line,
@@ -2098,16 +2167,16 @@ move_curr_line(view_t *view)
 	}
 	else if(pos > last)
 	{
-		scroll_down(view, pos - last);
+		fview_scroll_fwd_by(view, pos - last);
 		redraw++;
 	}
 	else if(pos < view->top_line)
 	{
-		scroll_up(view, view->top_line - pos);
+		fview_scroll_back_by(view, view->top_line - pos);
 		redraw++;
 	}
 
-	if(consider_scroll_offset(view))
+	if(fview_enforce_scroll_offset(view))
 	{
 		redraw++;
 	}
diff --git a/src/ui/fileview.h b/src/ui/fileview.h
index 4195fb5..af8edd7 100644
--- a/src/ui/fileview.h
+++ b/src/ui/fileview.h
@@ -27,6 +27,14 @@
 struct dir_entry_t;
 struct view_t;
 
+/* Specials value that can be returned by fview_map_coordinates(). */
+enum
+{
+	FVM_NONE  = -1, /* No item under the cursor, empty space. */
+	FVM_LEAVE = -2, /* Leave current directory. */
+	FVM_OPEN  = -3, /* Open current item. */
+};
+
 /* Packet set of parameters to pass as user data for processing columns. */
 typedef struct
 {
@@ -90,43 +98,33 @@ void fview_draw_inactive_cursor(struct view_t *view);
 /* Redraws cursor of the view on the screen. */
 void fview_cursor_redraw(struct view_t *view);
 
-/* Scrolling related functions. */
-
-/* Checks if view can be scrolled up (there are more files).  Returns non-zero
- * if so, and zero otherwise. */
-int can_scroll_up(const struct view_t *view);
+/* Clears miller-view preview if it's currently visible. */
+void fview_clear_miller_preview(struct view_t *view);
 
-/* Checks if view can be scrolled down (there are more files).  Returns non-zero
- * if so, and zero otherwise. */
-int can_scroll_down(const struct view_t *view);
-
-/* Scrolls view up at least by specified number of files.  Updates both top and
- * cursor positions. */
-void scroll_up(struct view_t *view, int by);
+/* Scrolling related functions. */
 
-/* Scrolls view down at least by specified number of files.  Updates both top
+/* Scrolls view up by at least the specified number of files.  Updates both top
  * and cursor positions. */
-void scroll_down(struct view_t *view, int by);
+void fview_scroll_back_by(struct view_t *view, int by);
 
-/* Calculates list position corrected for scrolling down.  Returns adjusted
- * position. */
-int get_corrected_list_pos_down(const struct view_t *view, int pos_delta);
+/* Scrolls view down by at least the specified number of files.  Updates both
+ * top and cursor positions. */
+void fview_scroll_fwd_by(struct view_t *view, int by);
 
-/* Calculates list position corrected for scrolling up.  Returns adjusted
- * position. */
-int get_corrected_list_pos_up(const struct view_t *view, int pos_delta);
+/* Scrolls view down or up by at least the specified number of files.  Updates
+ * both top and cursor positions.  A wrapper for fview_scroll_back_by() and
+ * fview_scroll_fwd_by() functions. */
+void fview_scroll_by(struct view_t *view, int by);
 
 /* Updates current and top line of a view according to 'scrolloff' option value.
  * Returns non-zero if redraw is needed. */
-int consider_scroll_offset(struct view_t *view);
+int fview_enforce_scroll_offset(struct view_t *view);
 
-/* Scrolls view down or up at least by specified number of files.  Updates both
- * top and cursor positions.  A wrapper for scroll_up() and scroll_down()
- * functions. */
-void scroll_by_files(struct view_t *view, int by);
+/* Scrolls the view one page up. */
+void fview_scroll_page_up(struct view_t *view);
 
-/* Recalculates difference of two panes scroll positions. */
-void update_scroll_bind_offset(void);
+/* Scrolls the view one page down. */
+void fview_scroll_page_down(struct view_t *view);
 
 /* Layout related functions. */
 
@@ -143,6 +141,10 @@ int fview_previews(struct view_t *view, const char path[]);
 /* Enables/disables cascading columns style of the view. */
 void fview_set_millerview(struct view_t *view, int enabled);
 
+/* Maps coordinate to file list position.  Returns position (>= 0) or FVM_*
+ * special value. */
+int fview_map_coordinates(struct view_t *view, int x, int y);
+
 /* Requests update of view geometry properties (stuff that depends on
  * dimensions; there is also an implicit dependency on file list, because grid
  * is defined by longest file name). */
diff --git a/src/ui/quickview.c b/src/ui/quickview.c
index c5f3bda..52ab536 100644
--- a/src/ui/quickview.c
+++ b/src/ui/quickview.c
@@ -863,7 +863,8 @@ qv_expand_viewer(view_t *view, const char viewer[], MacroFlags *flags)
 	char *result;
 	if(strchr(viewer, '%') == NULL)
 	{
-		char *escaped = shell_like_escape(get_current_file_name(view), 0);
+		char *escaped =
+			shell_arg_escape(get_current_file_name(view), curr_stats.shell_type);
 		result = format_str("%s %s", viewer, escaped);
 		free(escaped);
 	}
diff --git a/src/ui/statusbar.c b/src/ui/statusbar.c
index aea14dd..a01c97b 100644
--- a/src/ui/statusbar.c
+++ b/src/ui/statusbar.c
@@ -109,7 +109,7 @@ ui_sb_quick_msg_clear(void)
 		ui_sb_quick_msgf("%s", "");
 	}
 
-	if(vle_mode_is(CMDLINE_MODE))
+	if(modes_is_cmdline_like())
 	{
 		/* Restore previous contents of the status bar. */
 		stats_redraw_later();
diff --git a/src/ui/statusline.c b/src/ui/statusline.c
index 91105c5..a4f9a0c 100644
--- a/src/ui/statusline.c
+++ b/src/ui/statusline.c
@@ -24,7 +24,7 @@
 #include <assert.h> /* assert() */
 #include <ctype.h> /* isdigit() */
 #include <stddef.h> /* NULL size_t */
-#include <stdlib.h> /* RAND_MAX free() rand() */
+#include <stdlib.h> /* free() */
 #include <string.h> /* strcat() strdup() strlen() */
 #include <time.h> /* time() */
 #include <unistd.h>
@@ -486,8 +486,6 @@ parse_view_macros(view_t *view, const char **format, const char macros[],
 					 * closing brackets */
 					const char *e = strchr(*format, '}');
 					char *expr = NULL, *resstr = NULL;
-					var_t res = var_false();
-					ParsingErrors parsing_error;
 
 					/* If there's no matching closing bracket, just add the opening one
 					 * literally */
@@ -508,11 +506,11 @@ parse_view_macros(view_t *view, const char **format, const char macros[],
 					}
 					memcpy(expr, *format, e - (*format));
 
-					/* Try to parse expr, and convert the res to string if succeed. */
-					parsing_error = parse(expr, 0, &res);
-					if(parsing_error == PE_NO_ERROR)
+					/* Try to parse expr and convert the result to string on success. */
+					parsing_result_t result = vle_parser_eval(expr, /*interactive=*/0);
+					if(result.error == PE_NO_ERROR)
 					{
-						resstr = var_to_str(res);
+						resstr = var_to_str(result.value);
 					}
 
 					if(resstr != NULL)
@@ -524,7 +522,7 @@ parse_view_macros(view_t *view, const char **format, const char macros[],
 						copy_str(buf, sizeof(buf), "<Invalid expr>");
 					}
 
-					var_free(res);
+					var_free(result.value);
 					free(resstr);
 					free(expr);
 
@@ -532,13 +530,13 @@ parse_view_macros(view_t *view, const char **format, const char macros[],
 				}
 				break;
 			case '*':
-				if(width > 9)
+				if(width > LAST_USER_COLOR)
 				{
 					snprintf(buf, sizeof(buf), "%%%d*", (int)width);
 					width = 0;
 					break;
 				}
-				cline_set_attr(&result, '0' + width);
+				cline_set_attr(&result, /*user_color=*/width);
 				width = 0;
 				break;
 
@@ -620,6 +618,7 @@ get_tip(void)
 	  ":copen reopens the last list of files found by a :grep-like command",
 	  "Up/down arrows on command-line load history entries with identical prefix",
 	  ":copy/:move commands accept absolute paths",
+	  "Using dp on blank compare entry removes the other file",
 	};
 
 	if(need_to_shuffle)
@@ -629,8 +628,7 @@ get_tip(void)
 		unsigned int i;
 		for(i = 0U; i < ARRAY_LEN(tips) - 1U; ++i)
 		{
-			const unsigned int j =
-				i + (rand()/(RAND_MAX + 1.0))*(ARRAY_LEN(tips) - i);
+			const unsigned int j = vifm_rand(i, ARRAY_LEN(tips) - 1);
 			const char *const t = tips[i];
 			tips[i] = tips[j];
 			tips[j] = t;
@@ -941,7 +939,7 @@ is_job_bar_visible(void)
 {
 	/* Pretend that bar isn't visible in tests. */
 	return curr_stats.load_stage >= 2
-	    && ui_stat_job_bar_height() != 0 && !is_in_menu_like_mode();
+	    && ui_stat_job_bar_height() != 0 && !modes_is_menu_like();
 }
 
 void
@@ -978,11 +976,15 @@ format_job_bar(void)
 	size_t max_width;
 	char **descrs;
 
-	descrs = take_job_descr_snapshot();
-
 	bar_text[0] = '\0';
 	text_width = 0U;
 
+	descrs = take_job_descr_snapshot();
+	if(descrs == NULL)
+	{
+		return bar_text;
+	}
+
 	/* The check of stage is for tests. */
 	max_width = (curr_stats.load_stage < 2) ? 80 : getmaxx(job_bar);
 	width_used = 0U;
@@ -1021,22 +1023,26 @@ format_job_bar(void)
 }
 
 /* Makes snapshot of current job descriptions.  Returns array of length
- * nbar_jobs which should be freed via free_string_array(). */
+ * nbar_jobs which should be freed via free_string_array() or NULL.  The array
+ * can contain NULLs. */
 static char **
 take_job_descr_snapshot(void)
 {
-	size_t i;
-	char **descrs;
+	char **descrs = reallocarray(NULL, nbar_jobs, sizeof(*descrs));
+	if(descrs == NULL)
+	{
+		return NULL;
+	}
 
-	descrs = reallocarray(NULL, nbar_jobs, sizeof(*descrs));
+	size_t i;
 	for(i = 0U; i < nbar_jobs; ++i)
 	{
-		const char *descr;
-
-		bg_op_lock(bar_jobs[i]);
-		descr = bar_jobs[i]->descr;
-		descrs[i] = strdup((descr == NULL) ? "UNKNOWN" : descr);
-		bg_op_unlock(bar_jobs[i]);
+		if(bg_op_lock(bar_jobs[i]))
+		{
+			const char *descr = bar_jobs[i]->descr;
+			descrs[i] = strdup((descr == NULL) ? "UNKNOWN" : descr);
+			bg_op_unlock(bar_jobs[i]);
+		}
 	}
 
 	return descrs;
diff --git a/src/ui/tabs.c b/src/ui/tabs.c
index 6e422fe..ab7ff8d 100644
--- a/src/ui/tabs.c
+++ b/src/ui/tabs.c
@@ -260,9 +260,6 @@ clone_view(view_t *dst, view_t *side, const char path[], int clean)
 			path == NULL ? flist_get_dir(side) : path);
 
 	flist_init_view(dst);
-	/* This is for replace_dir_entries() below due to check in fentry_free(),
-	 * should adjust the check instead? */
-	dst->dir_entry[0].origin = side->curr_dir;
 
 	clone_local_options(side, dst, 1);
 	reset_local_options(dst);
diff --git a/src/ui/ui.c b/src/ui/ui.c
index 5e2302d..21fd6bf 100644
--- a/src/ui/ui.c
+++ b/src/ui/ui.c
@@ -48,10 +48,12 @@
 #include "../compat/pthread.h"
 #include "../engine/mode.h"
 #include "../int/term_title.h"
+#include "../lua/vlua.h"
 #include "../modes/dialogs/msg_dialog.h"
 #include "../modes/modes.h"
 #include "../modes/view.h"
 #include "../modes/wk.h"
+#include "../utils/darray.h"
 #include "../utils/fs.h"
 #include "../utils/log.h"
 #include "../utils/macros.h"
@@ -61,6 +63,7 @@
 #include "../utils/string_array.h"
 #include "../utils/utf8.h"
 #include "../utils/utils.h"
+#include "../compare.h"
 #include "../event_loop.h"
 #include "../filelist.h"
 #include "../flist_sel.h"
@@ -80,6 +83,17 @@
 #include "statusline.h"
 #include "tabs.h"
 
+/* List of formatted tab labels with some extra information. */
+typedef struct
+{
+	cline_t *labels; /* List of labels (might miss some tabs). */
+	int count;       /* Number of labels. */
+	int skipped;     /* Number of leading tabs skipped due to lack of space. */
+	int current;     /* Index of the current tab (does not take skipped count into
+	                    account). */
+}
+tab_line_info_t;
+
 /* Information for formatting tab title. */
 typedef struct
 {
@@ -129,8 +143,7 @@ WINDOW *menu_win;
 WINDOW *sort_win;
 WINDOW *change_win;
 WINDOW *error_win;
-
-static WINDOW *tab_line;
+WINDOW *tab_line;
 
 static WINDOW *lborder;
 static WINDOW *mborder;
@@ -167,6 +180,8 @@ static int view_shows_tabline(const view_t *view);
 static int get_tabline_height(void);
 static void print_tabline(WINDOW *win, view_t *view, col_attr_t base_col,
 		path_func pf);
+static tab_line_info_t format_tab_labels(view_t *view, int max_width,
+		path_func pf);
 static void compute_avg_width(int *avg_width, int *spare_width,
 		int min_widths[], int max_width, view_t *view, path_func pf);
 TSTATIC cline_t make_tab_title(const tab_title_info_t *title_info);
@@ -230,7 +245,7 @@ setup_ncurses_interface(void)
 	nonl();
 	raw();
 
-	curs_set(0);
+	ui_set_cursor(/*visibility=*/0);
 
 	getmaxyx(stdscr, screen_y, screen_x);
 	/* Screen is too small to be useful. */
@@ -372,6 +387,25 @@ move_pair(int from, int to)
 	}
 }
 
+void
+ui_set_mouse_active(int active)
+{
+	if(vifm_testing())
+	{
+		return;
+	}
+
+	if(active)
+	{
+		mousemask(ALL_MOUSE_EVENTS, /*oldmask=*/NULL);
+		mouseinterval(0);
+	}
+	else
+	{
+		mousemask(/*newmask=*/0, /*oldmask=*/NULL);
+	}
+}
+
 /* Initializes all WINDOW variables by calling newwin() to create ncurses
  * windows and configures hardware cursor. */
 static void
@@ -714,6 +748,29 @@ cv_tree(CVType type)
 	return type == CV_TREE || type == CV_CUSTOM_TREE;
 }
 
+const char *
+cv_describe(CVType type)
+{
+	switch(type)
+	{
+		case CV_REGULAR:
+			return "custom";
+
+		case CV_VERY:
+			return "very-custom";
+
+		case CV_CUSTOM_TREE:
+		case CV_TREE:
+			return "tree";
+
+		case CV_DIFF:
+		case CV_COMPARE:
+			return "compare";
+	}
+
+	return "UNKNOWN";
+}
+
 void
 update_screen(UpdateType update_kind)
 {
@@ -773,14 +830,7 @@ update_start(UpdateType update_kind)
 
 	if(curr_stats.save_msg == 0 && !ui_sb_multiline())
 	{
-		if(curr_view->selected_files)
-		{
-			print_selected_msg();
-		}
-		else
-		{
-			ui_sb_clear();
-		}
+		modes_statusbar_update();
 
 		if(vle_mode_is(VIEW_MODE))
 		{
@@ -884,7 +934,7 @@ ui_resize_all(void)
 
 	update_statusbar_layout();
 
-	curs_set(0);
+	ui_set_cursor(/*visibility=*/0);
 }
 
 /* Adjusts splitter position after screen resize. */
@@ -1083,7 +1133,7 @@ touch_all_windows(void)
 		return;
 	}
 
-	if(!is_in_menu_like_mode())
+	if(!modes_is_menu_like())
 	{
 		update_window_lazy(tab_line);
 
@@ -1383,7 +1433,10 @@ resize_for_menu_like(void)
 {
 	int screen_x, screen_y;
 
-	ui_update_term_state();
+	/* Resizing everything to avoid a situation when windows that are not visible
+	 * are of wrong size and using redrawwin() on them causes a memory corruption.
+	 * I'm pretty sure it's a bug in ncurses. */
+	ui_resize_all();
 	if(curr_stats.term_state == TS_TOO_SMALL)
 	{
 		return 1;
@@ -1414,7 +1467,7 @@ ui_setup_for_menu_like(void)
 	if(curr_stats.load_stage > 0)
 	{
 		scrollok(menu_win, FALSE);
-		curs_set(0);
+		ui_set_cursor(/*visibility=*/0);
 		werase(menu_win);
 		werase(status_bar);
 		werase(ruler_win);
@@ -1620,6 +1673,9 @@ ui_swap_view_data(view_t *left, view_t *right)
 	WINDOW *tmp;
 	int t;
 
+	/* Data in some fields doesn't need to be swapped.  Swap it beforehand so that
+	 * swapping structures will put it back. */
+
 	tmp = left->win;
 	left->win = right->win;
 	right->win = tmp;
@@ -1636,6 +1692,24 @@ ui_swap_view_data(view_t *left, view_t *right)
 	left->title = right->title;
 	right->title = tmp;
 
+	/* Swap these fields so they reflect updated layout. */
+
+	t = left->custom.diff_stats.unique_left;
+	left->custom.diff_stats.unique_left = left->custom.diff_stats.unique_right;
+	left->custom.diff_stats.unique_right = t;
+
+	t = right->custom.diff_stats.unique_left;
+	right->custom.diff_stats.unique_left = right->custom.diff_stats.unique_right;
+	right->custom.diff_stats.unique_right = t;
+
+	const int unique_lr = (CF_SHOW_UNIQUE_LEFT | CF_SHOW_UNIQUE_RIGHT);
+	if((left->custom.diff_cmp_flags & unique_lr) == CF_SHOW_UNIQUE_LEFT ||
+			(left->custom.diff_cmp_flags & unique_lr) == CF_SHOW_UNIQUE_RIGHT)
+	{
+		left->custom.diff_cmp_flags ^= unique_lr;
+		right->custom.diff_cmp_flags ^= unique_lr;
+	}
+
 	tmp_view = *left;
 	*left = *right;
 	*right = tmp_view;
@@ -1696,6 +1770,14 @@ move_splitter(int by, int fact)
 	set_splitter(pos + fact*by);
 }
 
+void
+ui_remember_scroll_offset(void)
+{
+	const int rwin_pos = rwin.top_line/rwin.column_count;
+	const int lwin_pos = lwin.top_line/lwin.column_count;
+	curr_stats.scroll_bind_off = rwin_pos - lwin_pos;
+}
+
 void
 ui_view_resize(view_t *view, int to)
 {
@@ -1859,6 +1941,97 @@ checked_wmove(WINDOW *win, int y, int x)
 	}
 }
 
+void
+ui_set_cursor(int visibility)
+{
+	/* PDCurses crashes if curs_set() is called for uninitialized library. */
+	if(!vifm_testing())
+	{
+		(void)curs_set(visibility);
+	}
+}
+
+int
+ui_get_mouse(MEVENT *event)
+{
+	int ret = getmouse(event);
+	if(ret != OK)
+	{
+		return ret;
+	}
+
+	/* Positions after 222 can become negative due to a combination of protocol
+	 * limitations and implementation.  This workaround can extend the range at
+	 * least a bit when SGR 1006 isn't available. */
+	if(event->x < 0)
+	{
+		event->x &= 0xff;
+	}
+	if(event->y < 0)
+	{
+		event->y &= 0xff;
+	}
+
+	int mask = M_ALL_MODES;
+	switch(vle_mode_get())
+	{
+		case NORMAL_MODE:  mask |= M_NORMAL_MODE; break;
+		case NAV_MODE:
+		case CMDLINE_MODE: mask |= M_CMDLINE_MODE; break;
+		case VISUAL_MODE:  mask |= M_VISUAL_MODE; break;
+		case MENU_MODE:    mask |= M_MENU_MODE; break;
+		case VIEW_MODE:    mask |= M_VIEW_MODE; break;
+
+		default:           mask = 0; break;
+	}
+
+	return (cfg.mouse & mask ? OK : ERR);
+}
+
+WINDOW *
+ui_get_tab_line_win(const view_t *view)
+{
+	return (view_shows_tabline(view) ? view->title : tab_line);
+}
+
+int
+ui_map_tab_line(view_t *view, int x)
+{
+	if(!is_null_or_empty(cfg.tab_line))
+	{
+		/* No mouse mapping for custom tabline. */
+		return -1;
+	}
+
+	path_func pf = cfg.shorten_title_paths ? &replace_home_part : &path_identity;
+
+	const int max_width = getmaxx(ui_get_tab_line_win(view));
+	tab_line_info_t info = format_tab_labels(view, max_width, pf);
+
+	int tab_idx = -1;
+
+	int i;
+	for(i = 0; i < info.count; ++i)
+	{
+		if(tab_idx == -1 && x < (int)info.labels[i].attrs_len)
+		{
+			tab_idx = info.skipped + i;
+		}
+		x -= info.labels[i].attrs_len;
+
+		cline_dispose(&info.labels[i]);
+	}
+	free(info.labels);
+
+	return tab_idx;
+}
+
+int
+ui_wenclose(const view_t *view, WINDOW *win, int x, int y)
+{
+	return (ui_view_is_visible(view) && wenclose(win, y, x));
+}
+
 void
 ui_display_too_small_term_msg(void)
 {
@@ -2016,41 +2189,95 @@ get_tabline_height(void)
 static void
 print_tabline(WINDOW *win, view_t *view, col_attr_t base_col, path_func pf)
 {
-	int i;
-	tab_info_t tab_info;
-
 	const int max_width = (vifm_testing() ? cfg.columns : getmaxx(win));
-	int width_used = 0;
-	int avg_width, spare_width;
-
-	int min_widths[tabs_count(view)];
 
 	ui_set_bg(win, &base_col, -1);
 	werase(win);
 	checked_wmove(win, 0, 0);
 
-	compute_avg_width(&avg_width, &spare_width, min_widths, max_width, view, pf);
+	if(is_null_or_empty(cfg.tab_line))
+	{
+		tab_line_info_t info = format_tab_labels(view, max_width, pf);
+
+		int i;
+		for(i = 0; i < info.count; ++i)
+		{
+			col_attr_t col = base_col;
+			if(i == info.current - info.skipped)
+			{
+				cs_mix_colors(&col, &cfg.cs.color[TAB_LINE_SEL_COLOR]);
+			}
+
+			cline_print(&info.labels[i], win, &col);
+			cline_dispose(&info.labels[i]);
+		}
+		free(info.labels);
+	}
+	else
+	{
+		char *fmt;
+		if(vlua_handler_cmd(curr_stats.vlua, cfg.tab_line))
+		{
+			int other = (view == other_view);
+			fmt = vlua_make_tab_line(curr_stats.vlua, cfg.tab_line, other, max_width);
+		}
+		else
+		{
+			fmt = strdup(cfg.tab_line);
+		}
+
+		if(fmt != NULL)
+		{
+			cline_t title = ma_expand_colored_custom(fmt, /*nmacros=*/0,
+					/*macros=*/NULL, MA_OPT);
+			free(fmt);
 
+			cline_print(&title, win, &base_col);
+			cline_dispose(&title);
+		}
+	}
+
+	wnoutrefresh(win);
+}
+
+/* Computes layout of the tab line and formats tab labels.  Returns list of tab
+ * labels along with supplementary information. */
+static tab_line_info_t
+format_tab_labels(view_t *view, int max_width, path_func pf)
+{
+	int i;
 	int tab_count = tabs_count(view);
+	int width_used = 0;
+
+	int avg_width, spare_width;
+	int min_widths[tab_count];
+	compute_avg_width(&avg_width, &spare_width, min_widths, max_width, view, pf);
+
 	int min_width = 0;
 	for(i = 0; i < tab_count; ++i)
 	{
 		min_width += min_widths[i];
 	}
 
-	int before_current = 1;
+	cline_t *tab_labels = NULL;
+	DA_INSTANCE(tab_labels);
 
+	int current_tab = -1;
+	int skipped_tabs = 0;
+	tab_info_t tab_info;
 	for(i = 0; tabs_get(view, i, &tab_info) && width_used < max_width; ++i)
 	{
 		int current = (tab_info.view == view);
 		if(current)
 		{
-			before_current = 0;
+			current_tab = i;
 		}
-		else if(before_current && min_width > max_width)
+		else if(current_tab == -1 && min_width > max_width)
 		{
+			/* Skip a tab that precedes the current one because it doesn't fit in. */
 			min_width -= min_widths[i];
 			spare_width = max_width - min_width;
+			++skipped_tabs;
 			continue;
 		}
 
@@ -2065,12 +2292,6 @@ print_tabline(WINDOW *win, view_t *view, col_attr_t base_col, path_func pf)
 		const int extra_width = prefix.attrs_len + suffix.attrs_len;
 		int width = max_width;
 
-		col_attr_t col = base_col;
-		if(current)
-		{
-			cs_mix_colors(&col, &cfg.cs.color[TAB_LINE_SEL_COLOR]);
-		}
-
 		if(!current)
 		{
 			width = tab_info.last ? (max_width - width_used)
@@ -2089,8 +2310,6 @@ print_tabline(WINDOW *win, view_t *view, col_attr_t base_col, path_func pf)
 					curr_stats.ellipsis);
 		}
 
-		ui_set_attr(win, &col, -1);
-
 		int real_width = prefix.attrs_len + title.attrs_len + suffix.attrs_len;
 
 		if(width < real_width && max_width - width_used >= real_width)
@@ -2099,19 +2318,26 @@ print_tabline(WINDOW *win, view_t *view, col_attr_t base_col, path_func pf)
 		}
 		if(width >= real_width)
 		{
-			cline_print(&prefix, win, &col);
-			cline_print(&title, win, &col);
-			cline_print(&suffix, win, &col);
+			cline_t *tab_label = DA_EXTEND(tab_labels);
+			if(tab_label != NULL)
+			{
+				*tab_label = prefix;
+				cline_append(tab_label, &title);
+				cline_append(tab_label, &suffix);
+				DA_COMMIT(tab_labels);
+			}
 		}
 
 		width_used += real_width;
-
-		cline_dispose(&prefix);
-		cline_dispose(&title);
-		cline_dispose(&suffix);
 	}
 
-	wnoutrefresh(win);
+	tab_line_info_t result = {
+		.labels = tab_labels,
+		.count = DA_SIZE(tab_labels),
+		.skipped = skipped_tabs,
+		.current = current_tab,
+	};
+	return result;
 }
 
 /* Computes average width of tab tips as well as number of spare character
@@ -2515,13 +2741,21 @@ int
 ui_view_right_reserved(const view_t *view)
 {
 	dir_entry_t *const entry = get_current_entry(view);
+
+	if(!is_in_miller_view(view) || is_parent_dir(entry->name))
+	{
+		return 0;
+	}
+
+	if(view->miller_preview != MP_ALL &&
+			fentry_is_dir(entry) != (view->miller_preview == MP_DIRS))
+	{
+		return 0;
+	}
+
 	const int total = view->miller_ratios[0] + view->miller_ratios[1]
 	                + view->miller_ratios[2];
-	return is_in_miller_view(view)
-	    && !is_parent_dir(entry->name)
-	    && (fentry_is_dir(entry) || view->miller_preview_files)
-	     ? (view->window_cols*view->miller_ratios[2])/total
-	     : 0;
+	return (view->window_cols*view->miller_ratios[2])/total;
 }
 
 /* Whether miller columns should be displayed.  Returns non-zero if so,
@@ -2577,6 +2811,15 @@ ui_qv_cleanup_if_needed(void)
 	{
 		qv_cleanup(other_view, curr_stats.preview.cleanup_cmd);
 	}
+
+	if(ui_view_is_visible(&lwin))
+	{
+		fview_clear_miller_preview(&lwin);
+	}
+	if(ui_view_is_visible(&rwin))
+	{
+		fview_clear_miller_preview(&rwin);
+	}
 }
 
 void
@@ -2647,13 +2890,22 @@ ui_shutdown(void)
 void
 ui_pause(void)
 {
+#ifdef _WIN32
+	/* Refresh the window, because otherwise curses redraws the screen on call to
+	 * `compat_wget_wch()` (why does it do this?). */
+	use_wrefresh(inf_delay_window);
+#endif
+
 	/* Show previous screen state. */
 	ui_shutdown();
+
+#ifndef _WIN32
 	/* Yet restore program mode to read input without waiting for Enter. */
 	reset_prog_mode();
 	/* Refresh the window, because otherwise curses redraws the screen on call to
 	 * `compat_wget_wch()` (why does it do this?). */
 	wnoutrefresh(inf_delay_window);
+#endif
 
 	/* Ignore window resize. */
 	wint_t pressed;
diff --git a/src/ui/ui.h b/src/ui/ui.h
index 2a7a4ec..91c90eb 100644
--- a/src/ui/ui.h
+++ b/src/ui/ui.h
@@ -165,6 +165,15 @@ typedef enum
 }
 CompareType;
 
+/* Type of files to list after a comparison. */
+typedef enum
+{
+	LT_ALL,    /* All files. */
+	LT_DUPS,   /* Files that have at least 1 dup on other side or in this view. */
+	LT_UNIQUE, /* Files unique to this view or within this view. */
+}
+ListType;
+
 /* Type of scheduled view update event. */
 typedef enum
 {
@@ -183,6 +192,15 @@ typedef enum
 }
 NameFormat;
 
+/* What kinds of entries should be previewed by miller views. */
+typedef enum
+{
+	MP_ALL,   /* All entries. */
+	MP_DIRS,  /* Directories only. */
+	MP_FILES, /* Files only. */
+}
+MillerPreview;
+
 /* Single entry of directory history. */
 typedef struct
 {
@@ -254,15 +272,27 @@ typedef struct
 }
 entries_t;
 
+/* Entry with comparison results. */
+typedef struct
+{
+	int identical;    /* Number of matched files judged identical. */
+	int different;    /* Number of matched files judged different. */
+	int unique_left;  /* Number of unmatched files on the left. */
+	int unique_right; /* Number of unmatched files on the right. */
+}
+compare_stats_t;
+
 /* Data related to custom filling. */
 struct cv_data_t
 {
 	/* Type of the custom view. */
 	CVType type;
 
-	/* Additional data about CV_DIFF type. */
-	CompareType diff_cmp_type; /* Type of comparison. */
-	int diff_path_group;       /* Whether entries are grouped by paths. */
+	/* Additional data for CV_DIFF type. */
+	CompareType diff_cmp_type;  /* Type of comparison. */
+	ListType diff_list_type;    /* Type of results. */
+	int diff_cmp_flags;         /* Flags used to build the diff. */
+	compare_stats_t diff_stats; /* List of comparison results */
 
 	/* This is temporary storage for custom list entries used during its
 	 * construction. */
@@ -353,8 +383,8 @@ struct view_t
 	int miller_view, miller_view_g;
 	/* Proportions of columns. */
 	int miller_ratios[3], miller_ratios_g[3];
-	/* Whether right column should also preview files. */
-	int miller_preview_files, miller_preview_files_g;
+	/* What entries are to be previewed in the right column. */
+	MillerPreview miller_preview, miller_preview_g;
 	/* Caches of file lists for miller mode. */
 	cached_entries_t left_column;
 	cached_entries_t right_column;
@@ -416,7 +446,7 @@ struct view_t
 	signed char sort[SK_COUNT], sort_g[SK_COUNT];
 	/* Sorting groups (comma-separated list of regular expressions). */
 	char *sort_groups, *sort_groups_g;
-	/* Primary group in compiled form. */
+	/* Primary group of sort_groups (not sort_groups_g) in compiled form. */
 	regex_t primary_group;
 
 	int history_num;    /* Number of used history elements. */
@@ -438,6 +468,7 @@ struct view_t
 	/* ls-like view related fields. */
 	int ls_view, ls_view_g;             /* Non-zero if ls-like view is enabled. */
 	int ls_transposed, ls_transposed_g; /* Non-zero for transposed ls-view. */
+	int ls_cols, ls_cols_g;             /* Non-zero if column count is limited. */
 	size_t max_filename_width; /* Maximum filename width (length in character
 	                            * positions on the screen) among all entries of
 	                            * the file list.  Zero if not calculated. */
@@ -485,6 +516,7 @@ extern WINDOW *menu_win;
 extern WINDOW *sort_win;
 extern WINDOW *change_win;
 extern WINDOW *error_win;
+extern WINDOW *tab_line;
 
 /* Updates the ruler with information from the view (possibly lazily). */
 void ui_ruler_update(view_t *view, int lazy_redraw);
@@ -524,6 +556,10 @@ int cv_compare(CVType type);
  * non-zero if so, otherwise zero is returned. */
 int cv_tree(CVType type);
 
+/* Retrieves textual description of the specified custom view type.  Returns the
+ * description. */
+const char * cv_describe(CVType type);
+
 /* Resizes all windows according to current screen size and TUI
  * configuration. */
 void ui_resize_all(void);
@@ -618,6 +654,9 @@ void only(void);
  * given factor. */
 void move_splitter(int by, int fact);
 
+/* Recalculates difference of two panes scroll positions. */
+void ui_remember_scroll_offset(void);
+
 /* Sets size of the view to specified value. */
 void ui_view_resize(view_t *view, int to);
 
@@ -638,6 +677,25 @@ void ui_view_reset_decor_cache(const view_t *view);
  * movement. */
 void checked_wmove(WINDOW *win, int y, int x);
 
+/* Changes visibility of hardware cursor. */
+void ui_set_cursor(int visibility);
+
+/* Retrieves mouse event.  Adjusts and filters events in the process.  Returns
+ * ERR or OK curses error codes. */
+int ui_get_mouse(MEVENT *event);
+
+/* Determines curses window that displays tabs for the view or global tabs if
+ * they are active.  Returns the window pointer. */
+WINDOW * ui_get_tab_line_win(const view_t *view);
+
+/* Determines index of a tab at the specified coordinate.  Returns tab number
+ * base zero or -1 if tab label is not present at that location. */
+int ui_map_tab_line(view_t *view, int x);
+
+/* Determines whether coordinates are with the window which is part of the view.
+ * Returns non-zero if so, otherwise zero is returned. */
+int ui_wenclose(const view_t *view, WINDOW *win, int x, int y);
+
 /* Displays "Terminal is too small" kind of message instead of UI. */
 void ui_display_too_small_term_msg(void);
 
@@ -744,6 +802,9 @@ int ui_view_unsorted(const view_t *view);
  * or when terminal might be used by another application that vifm runs). */
 void ui_shutdown(void);
 
+/* Enables/disables mouse support. */
+void ui_set_mouse_active(int active);
+
 /* Temporarily shuts down UI until a key is pressed. */
 void ui_pause(void);
 
diff --git a/src/utils/cancellation.c b/src/utils/cancellation.c
index 77e79f4..91b356f 100644
--- a/src/utils/cancellation.c
+++ b/src/utils/cancellation.c
@@ -25,13 +25,7 @@ const cancellation_t no_cancellation = {};
 int
 cancellation_requested(const cancellation_t *info)
 {
-	return cancellation_possible(info) && info->hook(info->arg);
-}
-
-int
-cancellation_possible(const cancellation_t *info)
-{
-	return info->hook != NULL;
+	return (info->hook != NULL && info->hook(info->arg));
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/src/utils/cancellation.h b/src/utils/cancellation.h
index a488610..93db41d 100644
--- a/src/utils/cancellation.h
+++ b/src/utils/cancellation.h
@@ -40,10 +40,6 @@ extern const cancellation_t no_cancellation;
  * non-zero if so, otherwise zero is returned.  */
 int cancellation_requested(const cancellation_t *info);
 
-/* Checks whether cancellation is enabled (i.e. hook is provided).  Returns
- * non-zero if so, otherwise zero is returned. */
-int cancellation_possible(const cancellation_t *info);
-
 #endif /* VIFM__UTILS__CANCELLATION_H__ */
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/src/utils/fs.c b/src/utils/fs.c
index 7521290..3f51421 100644
--- a/src/utils/fs.c
+++ b/src/utils/fs.c
@@ -32,7 +32,7 @@
 #include <unistd.h> /* pathconf() readlink() */
 
 #include <ctype.h> /* isalpha() */
-#include <errno.h> /* errno */
+#include <errno.h> /* EINVAL ERANGE errno */
 #include <stddef.h> /* NULL */
 #include <stdio.h> /* snprintf() remove() */
 #include <stdlib.h> /* free() */
@@ -799,6 +799,48 @@ restore_cwd(char saved_cwd[])
 	}
 }
 
+FILE *
+make_tmp_file(char path[], mode_t mode, int auto_delete)
+{
+	int fd = create_unique_file(path, mode, auto_delete);
+	if(fd == -1)
+	{
+		return NULL;
+	}
+
+	FILE *file = fdopen(fd, "w+b");
+	if(file == NULL)
+	{
+		int error = errno;
+		(void)close(fd);
+		errno = error;
+	}
+
+	return file;
+}
+
+FILE *
+make_file_in_tmp(const char prefix[], mode_t mode, int auto_delete,
+		char full_path[], size_t full_path_len)
+{
+	if(contains_slash(prefix))
+	{
+		errno = EINVAL;
+		return NULL;
+	}
+
+	int len = snprintf(full_path, full_path_len, "%s/%s-XXXXXX", get_tmpdir(),
+			prefix);
+	if(len < 0 || (size_t)len >= full_path_len)
+	{
+		errno = ERANGE;
+		return NULL;
+	}
+
+	system_to_internal_slashes(full_path);
+	return make_tmp_file(full_path, mode, auto_delete);
+}
+
 #ifndef _WIN32
 
 /* Checks if path (dereferenced for a symbolic link) is an existing directory.
diff --git a/src/utils/fs.h b/src/utils/fs.h
index 9915b14..aaa46bf 100644
--- a/src/utils/fs.h
+++ b/src/utils/fs.h
@@ -24,6 +24,7 @@
 
 #include <stddef.h> /* size_t */
 #include <stdint.h> /* uint32_t uint64_t */
+#include <stdio.h> /* FILE */
 
 /* Functions to deal with file system objects */
 
@@ -212,6 +213,17 @@ char * save_cwd(void);
  * was remembered, does nothing. */
 void restore_cwd(char saved_cwd[]);
 
+/* Creates a unique file by replacing trailing "XXXXXX" in the path with some
+ * unique sequence.  Updates the path in place.  Returns file descriptor on
+ * success or -1 on failure (including when there is no trailing "XXXXXX").
+ * errno is meaningful on failure. */
+FILE * make_tmp_file(char path[], mode_t mode, int auto_delete);
+
+/* Same as make_tmp_file(), but always creates a file in a temporary directory.
+ * The prefix must not contain slashes.  errno is meaningful on failure. */
+FILE * make_file_in_tmp(const char prefix[], mode_t mode, int auto_delete,
+		char full_path[], size_t full_path_len);
+
 #ifdef _WIN32
 
 int S_ISLNK(mode_t mode);
diff --git a/src/utils/parson.c b/src/utils/parson.c
index 734a209..806d989 100644
--- a/src/utils/parson.c
+++ b/src/utils/parson.c
@@ -289,7 +289,10 @@ static char * read_file(const char * filename) {
     if (!fp) {
         return NULL;
     }
-    fseek(fp, 0L, SEEK_END);
+    if (fseek(fp, 0L, SEEK_END) != 0) {
+        fclose(fp);
+        return NULL;
+    }
     pos = ftell(fp);
     if (pos < 0) {
         fclose(fp);
@@ -883,7 +886,7 @@ static JSON_Value * parse_null_value(const char **string) {
 
 static int json_serialize_to_buffer_r(const JSON_Value *value, char *buf, int level, int is_pretty, char *num_buf)
 {
-    const char *key = NULL, *string = NULL;
+    const char *key = NULL;
     JSON_Value *temp_value = NULL;
     JSON_Array *array = NULL;
     JSON_Object *object = NULL;
@@ -891,10 +894,10 @@ static int json_serialize_to_buffer_r(const JSON_Value *value, char *buf, int le
     double num = 0.0;
     int written = -1, written_total = 0;
 
-    switch (json_value_get_type(value)) {
+    switch (value->type) {
         case JSONArray:
-            array = json_value_get_array(value);
-            count = json_array_get_count(array);
+            array = value->value.array;
+            count = array->count;
             APPEND_STRING("[");
             if (count > 0 && is_pretty) {
                 APPEND_STRING("\n");
@@ -903,7 +906,7 @@ static int json_serialize_to_buffer_r(const JSON_Value *value, char *buf, int le
                 if (is_pretty) {
                     APPEND_INDENT(level+1);
                 }
-                temp_value = json_array_get_value(array, i);
+                temp_value = array->items[i];
                 written = json_serialize_to_buffer_r(temp_value, buf, level+1, is_pretty, num_buf);
                 if (written < 0) {
                     return -1;
@@ -925,14 +928,14 @@ static int json_serialize_to_buffer_r(const JSON_Value *value, char *buf, int le
             APPEND_STRING("]");
             return written_total;
         case JSONObject:
-            object = json_value_get_object(value);
-            count  = json_object_get_count(object);
+            object = value->value.object;
+            count  = object->count;
             APPEND_STRING("{");
             if (count > 0 && is_pretty) {
                 APPEND_STRING("\n");
             }
             for (i = 0; i < count; i++) {
-                key = json_object_get_name(object, i);
+                key = object->names[i];
                 if (key == NULL) {
                     return -1;
                 }
@@ -951,7 +954,7 @@ static int json_serialize_to_buffer_r(const JSON_Value *value, char *buf, int le
                 if (is_pretty) {
                     APPEND_STRING(" ");
                 }
-                temp_value = json_object_get_value(object, key);
+                temp_value = object->values[i];
                 written = json_serialize_to_buffer_r(temp_value, buf, level+1, is_pretty, num_buf);
                 if (written < 0) {
                     return -1;
@@ -973,11 +976,7 @@ static int json_serialize_to_buffer_r(const JSON_Value *value, char *buf, int le
             APPEND_STRING("}");
             return written_total;
         case JSONString:
-            string = json_value_get_string(value);
-            if (string == NULL) {
-                return -1;
-            }
-            written = json_serialize_string(string, buf);
+            written = json_serialize_string(value->value.string, buf);
             if (written < 0) {
                 return -1;
             }
@@ -987,7 +986,7 @@ static int json_serialize_to_buffer_r(const JSON_Value *value, char *buf, int le
             written_total += written;
             return written_total;
         case JSONBoolean:
-            if (json_value_get_boolean(value)) {
+            if (value->value.boolean) {
                 APPEND_STRING("true");
             } else {
                 APPEND_STRING("false");
diff --git a/src/utils/path.c b/src/utils/path.c
index 3b8f73e..bb3c586 100644
--- a/src/utils/path.c
+++ b/src/utils/path.c
@@ -31,9 +31,9 @@
 #include <errno.h> /* errno */
 #include <stddef.h> /* NULL size_t */
 #include <stdio.h>  /* snprintf() */
-#include <stdlib.h> /* malloc() free() */
+#include <stdlib.h> /* free() */
 #include <string.h> /* memset() strcat() strcmp() strdup() strncmp() strncat()
-                       strchr() strcpy() strlen() strrchr() */
+                       strchr() strcpy() strlen() strpbrk() strrchr() */
 
 #include "../cfg/config.h"
 #include "../compat/fs_limits.h"
@@ -354,89 +354,6 @@ is_unc_root(const char *path)
 #endif
 }
 
-char *
-shell_like_escape(const char string[], int type)
-{
-	size_t len;
-	size_t i;
-	char *ret, *dup;
-
-	len = strlen(string);
-
-	dup = ret = malloc(len*3 + 2 + 1);
-	if(dup == NULL)
-	{
-		return NULL;
-	}
-
-	if(*string == '-')
-	{
-		*dup++ = '.';
-		*dup++ = '/';
-	}
-
-	for(i = 0; i < len; i++, string++, dup++)
-	{
-		switch(*string)
-		{
-			case '%':
-				if(type == 1)
-				{
-					*dup++ = '%';
-				}
-				break;
-
-			/* Escape the following characters anywhere in the line. */
-			case '\'':
-			case '\\':
-			case '\r':
-			case '\t':
-			case '"':
-			case ';':
-			case ' ':
-			case '?':
-			case '|':
-			case '[':
-			case ']':
-			case '{':
-			case '}':
-			case '<':
-			case '>':
-			case '`':
-			case '!':
-			case '$':
-			case '&':
-			case '*':
-			case '(':
-			case ')':
-			case '#':
-				*dup++ = '\\';
-				break;
-
-			case '\n':
-				if(type != 0)
-				{
-					break;
-				}
-
-				*dup++ = '"';
-				*dup++ = '\n';
-				*dup = '"';
-				continue;
-
-			/* Escape the following characters only at the beginning of the line. */
-			case '~':
-			case '=': /* Command-path expansion in zsh. */
-				if(dup == ret)
-					*dup++ = '\\';
-				break;
-		}
-		*dup = *string;
-	}
-	*dup = '\0';
-	return ret;
-}
-
 char *
 replace_home_part(const char path[])
 {
@@ -495,13 +412,6 @@ replace_tilde(char path[])
 static char *
 try_replace_tilde(const char path[])
 {
-#ifndef _WIN32
-	char name[NAME_MAX + 1];
-	const char *p;
-	char *result;
-	struct passwd *pw;
-#endif
-
 	if(path[0] != '~')
 	{
 		return (char *)path;
@@ -515,26 +425,24 @@ try_replace_tilde(const char path[])
 	}
 
 #ifndef _WIN32
-	if((p = strchr(path, '/')) == NULL)
-	{
-		p = path + strlen(path);
-		copy_str(name, sizeof(name), path + 1);
-	}
-	else
+	char user_name[NAME_MAX + 1];
+
+	const char *p = until_first(path + 1, '/');
+	int user_name_len = p - (path + 1);
+	if(user_name_len + 1 > (int)sizeof(user_name))
 	{
-		copy_str(name, p - (path + 1) + 1, path + 1);
-		p++;
+		return (char *)path;
 	}
 
-	if((pw = getpwnam(name)) == NULL)
+	copy_str(user_name, user_name_len + 1, path + 1);
+
+	struct passwd *pw = getpwnam(user_name);
+	if(pw == NULL)
 	{
 		return (char *)path;
 	}
 
-	chosp(pw->pw_dir);
-	result = join_paths(pw->pw_dir, p);
-
-	return result;
+	return join_paths(pw->pw_dir, p);
 #else
 	return (char *)path;
 #endif
@@ -652,14 +560,13 @@ to_canonic_path(const char path[], const char base[], char buf[],
 }
 
 int
-contains_slash(const char *path)
+contains_slash(const char path[])
 {
-	char *slash_pos = strchr(path, '/');
-#ifdef _WIN32
-	if(slash_pos == NULL)
-		slash_pos = strchr(path, '\\');
+#ifndef _WIN32
+	return (strchr(path, '/') != NULL);
+#else
+	return (strpbrk(path, "/\\") != NULL);
 #endif
-	return slash_pos != NULL;
 }
 
 char *
@@ -792,14 +699,6 @@ find_cmd_in_path(const char cmd[], size_t path_len, char path[])
 	return 1;
 }
 
-void
-generate_tmp_file_name(const char prefix[], char buf[], size_t buf_len)
-{
-	snprintf(buf, buf_len, "%s/%s", get_tmpdir(), prefix);
-	system_to_internal_slashes(buf);
-	copy_str(buf, buf_len, make_name_unique(buf));
-}
-
 const char *
 get_tmpdir(void)
 {
@@ -810,7 +709,15 @@ get_tmpdir(void)
 void
 build_path(char buf[], size_t buf_len, const char p1[], const char p2[])
 {
-	snprintf(buf, buf_len, "%s%s%s", p1, ends_with_slash(p1) ? "" : "/", p2);
+	p2 = skip_char(p2, '/');
+	if(p2[0] == '\0')
+	{
+		copy_str(buf, buf_len, p1);
+	}
+	else
+	{
+		snprintf(buf, buf_len, "%s%s%s", p1, ends_with_slash(p1) ? "" : "/", p2);
+	}
 }
 
 char *
diff --git a/src/utils/path.h b/src/utils/path.h
index 160f9fb..879d0cb 100644
--- a/src/utils/path.h
+++ b/src/utils/path.h
@@ -22,13 +22,6 @@
 
 #include <stddef.h> /* size_t */
 
-/* String with path items separator supported by the system. */
-#ifndef _WIN32
-#define PATH_SEPARATORS "/"
-#else
-#define PATH_SEPARATORS "/\\"
-#endif
-
 /* Various functions to work with paths */
 
 /* Like chomp() but removes trailing slashes. */
@@ -57,12 +50,6 @@ int is_root_dir(const char *path);
 
 int is_unc_root(const char *path);
 
-/* Escapes the string for the purpose of inserting it into the shell or
- * command-line.  type == 1 enables prepending percent sign with a percent
- * sign and not escaping newline, because we do only worse.  type == 2 only
- * skips escaping of newline.  Returns new string, caller should free it. */
-char * shell_like_escape(const char string[], int type);
-
 /* Replaces leading path to home directory with a tilde, trims trailing slash.
  * Returns pointer to a statically allocated buffer of size PATH_MAX. */
 char * replace_home_part(const char path[]);
@@ -99,15 +86,16 @@ void ensure_path_well_formed(char *path);
 void to_canonic_path(const char path[], const char base[], char buf[],
 		size_t buf_len);
 
-/* Checks if path contains slash (also checks for backward slash on Windows). */
-int contains_slash(const char *path);
+/* Checks if path contains slash (also checks for backward slash on Windows).
+ * Returns non-zero if so. */
+int contains_slash(const char path[]);
 
 /* Returns position of the last slash (including backward slash on Windows) in
  * the path. */
 char * find_slashr(const char *path);
 
 /* Removes extension part from the path and returns a pointer to the first
- * character of the extension part. */
+ * character of the extension part.  Considers .tar.* as an extension. */
 char * cut_extension(char path[]);
 
 /* Splits path into root and extension parts.  Sets *root_len to length of the
@@ -135,9 +123,6 @@ int is_builtin_dir(const char name[]);
  * non-zero is returned. */
 int find_cmd_in_path(const char cmd[], size_t path_len, char path[]);
 
-/* Generates file name inside temporary directory. */
-void generate_tmp_file_name(const char prefix[], char buf[], size_t buf_len);
-
 /* Uses environment variables to determine the correct place.  Returns path to
  * tmp directory. */
 const char * get_tmpdir(void);
diff --git a/src/utils/str.c b/src/utils/str.c
index db41e17..c45f80e 100644
--- a/src/utils/str.c
+++ b/src/utils/str.c
@@ -384,6 +384,18 @@ strnoscmp(const char *s, const char *t, size_t n)
 #endif
 }
 
+int
+strsorter(const void *s, const void *t)
+{
+	return strcmp(*(const char **)s, *(const char **)t);
+}
+
+int
+strcasesorter(const void *s, const void *t)
+{
+	return strcasecmp(*(const char **)s, *(const char **)t);
+}
+
 int
 strossorter(const void *s, const void *t)
 {
diff --git a/src/utils/str.h b/src/utils/str.h
index be7149d..c5a3ca5 100644
--- a/src/utils/str.h
+++ b/src/utils/str.h
@@ -131,6 +131,12 @@ int stroscmp(const char *s, const char *t);
 /* Compares part of strings in OS dependent way. */
 int strnoscmp(const char *s, const char *t, size_t n);
 
+/* Wraps strcmp() for use with qsort(). */
+int strsorter(const void *s, const void *t);
+
+/* Wraps strcasecmp() for use with qsort(). */
+int strcasesorter(const void *s, const void *t);
+
 /* Wraps stroscmp() for use with qsort(). */
 int strossorter(const void *s, const void *t);
 
diff --git a/src/utils/string_array.c b/src/utils/string_array.c
index 38b18ce..7db9ea5 100644
--- a/src/utils/string_array.c
+++ b/src/utils/string_array.c
@@ -205,9 +205,12 @@ read_file_of_lines(const char filepath[], int *nlines)
 {
 	size_t text_len;
 	char *const text = read_whole_file(filepath, &text_len);
-	char **list = (text == NULL)
-	            ? NULL
-	            : text_to_lines(text, text_len, nlines, 0);
+	if(text == NULL)
+	{
+		return NULL;
+	}
+
+	char **list = text_to_lines(text, text_len, nlines, 0);
 	if(list == NULL)
 	{
 		list = malloc(sizeof(*list));
@@ -438,22 +441,26 @@ break_into_lines(char text[], size_t text_len, int *nlines, int null_sep)
 int
 write_file_of_lines(const char filepath[], char *strs[], size_t nstrs)
 {
-	size_t i;
-
 	FILE *const fp = os_fopen(filepath, "w");
 	if(fp == NULL)
 	{
 		return 1;
 	}
 
+	write_lines_to_file(fp, strs, nstrs);
+	fclose(fp);
+	return 0;
+}
+
+void
+write_lines_to_file(FILE *fp, char *strs[], size_t nstrs)
+{
+	size_t i;
 	for(i = 0U; i < nstrs; ++i)
 	{
 		fputs(strs[i], fp);
 		putc('\n', fp);
 	}
-
-	fclose(fp);
-	return 0;
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/src/utils/string_array.h b/src/utils/string_array.h
index 81a3a5f..9d7fd72 100644
--- a/src/utils/string_array.h
+++ b/src/utils/string_array.h
@@ -118,6 +118,9 @@ char ** break_into_lines(char text[], size_t text_len, int *nlines,
  * otherwise non-zero is returned and errno contains error code. */
 int write_file_of_lines(const char filepath[], char *strs[], size_t nstrs);
 
+/* Writes all lines to the file stream. */
+void write_lines_to_file(FILE *fp, char *strs[], size_t nstrs);
+
 #endif /* VIFM__UTILS__STRING_ARRAY_H__ */
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/src/utils/trie.c b/src/utils/trie.c
index caddbc7..fcda257 100644
--- a/src/utils/trie.c
+++ b/src/utils/trie.c
@@ -97,6 +97,10 @@ trie_t *
 trie_create(trie_free_func free_func)
 {
 	trie_t *trie = calloc(1U, sizeof(*trie));
+	if(trie == NULL)
+	{
+		return NULL;
+	}
 	trie->free_func = free_func;
 	return trie;
 }
@@ -306,7 +310,6 @@ make_node(trie_t *trie)
 {
 	const int bank = trie->node_count/NODES_PER_BANK;
 	const int bank_index = trie->node_count%NODES_PER_BANK;
-	++trie->node_count;
 
 	if(bank_index == 0)
 	{
@@ -318,8 +321,13 @@ make_node(trie_t *trie)
 
 		trie->nodes = nodes;
 		trie->nodes[bank] = calloc(NODES_PER_BANK, sizeof(**trie->nodes));
+		if(trie->nodes[bank] == NULL)
+		{
+			return NULL;
+		}
 	}
 
+	++trie->node_count;
 	return &trie->nodes[bank][bank_index];
 }
 
diff --git a/src/utils/utf8.c b/src/utils/utf8.c
index e1e29ec..d3958f5 100644
--- a/src/utils/utf8.c
+++ b/src/utils/utf8.c
@@ -179,6 +179,21 @@ utf8_strsw(const char str[])
 	return length;
 }
 
+size_t
+utf8_nstrsw(const char str[], int len)
+{
+	size_t sw = 0;
+	const char *p = str;
+	while(*p != '\0' && p - str < len)
+	{
+		const size_t char_width = utf8_chrw(p);
+		const size_t char_screen_width = chrsw(p, char_width);
+		p += char_width;
+		sw += char_screen_width;
+	}
+	return sw;
+}
+
 size_t
 utf8_strsw_with_tabs(const char str[], int tab_stops)
 {
diff --git a/src/utils/utf8.h b/src/utils/utf8.h
index cf0c576..85ce92c 100644
--- a/src/utils/utf8.h
+++ b/src/utils/utf8.h
@@ -50,6 +50,12 @@ size_t utf8_nstrsnlen(const char str[], size_t max_screen_width);
  * number. */
 size_t utf8_strsw(const char str[]);
 
+/* Counts number of screen characters in a utf-8 encoded str of the specified
+ * length (or less if NUL byte is found earlier).  If length ends in the middle
+ * of a utf-8 character, the character's screen width is counted as well.
+ * Returns the number. */
+size_t utf8_nstrsw(const char str[], int len);
+
 /* Counts number of screen characters in a utf-8 encoded str expanding
  * tabulation according to specified tab stops.  tab_stops must be positive.
  * Returns the number. */
diff --git a/src/utils/utils.c b/src/utils/utils.c
index 38f4413..f9b5f87 100644
--- a/src/utils/utils.c
+++ b/src/utils/utils.c
@@ -31,12 +31,14 @@
 #include <sys/types.h> /* pid_t */
 #include <unistd.h>
 
+#include <assert.h> /* assert() */
 #include <ctype.h> /* isalnum() isalpha() iscntrl() */
-#include <errno.h> /* errno */
+#include <errno.h> /* EEXIST EINVAL errno */
 #include <math.h> /* modf() pow() */
 #include <stddef.h> /* size_t */
 #include <stdio.h> /* snprintf() */
-#include <stdlib.h> /* free() malloc() qsort() */
+#include <stdlib.h> /* RAND_MAX free() malloc() qsort() rand() random() srand()
+                       srandom() */
 #include <string.h> /* memcpy() strdup() strchr() strlen() strpbrk() strtol() */
 #include <time.h> /* tm localtime() strftime() */
 #include <wchar.h> /* wcwidth() */
@@ -65,6 +67,11 @@
 #include "string_array.h"
 #include "utf8.h"
 
+/* Prefer random() over rand() because the former is non-linear. */
+#if defined(HAVE_RANDOM) && defined(HAVE_SRANDOM)
+# define USE_POSIX_RANDOM
+#endif
+
 static void show_progress_cb(const void *descr);
 static const char ** get_size_suffixes(void);
 static double split_size_double(double d, unsigned long long *ifraction,
@@ -140,6 +147,7 @@ process_cmd_output(const char descr[], const char cmd[], FILE *input,
 		show_progress("", 0);
 	}
 
+	/* XXX: reading can potentially never end if error pipe gets filled. */
 	wait_for_data_from(pid, file, 0, &ui_cancellation_info);
 	lines = read_stream_lines(file, &nlines, 1,
 			interactive ? NULL : &show_progress_cb, descr);
@@ -220,7 +228,7 @@ expand_envvars(const char str[], int flags)
 
 				if(escape_vals)
 				{
-					escaped_var_value = shell_like_escape(var_value, 2);
+					escaped_var_value = posix_like_escape(var_value, /*type=*/2);
 					var_value = escaped_var_value;
 				}
 
@@ -361,7 +369,7 @@ enclose_in_dquotes(const char str[], ShellType shell_type)
 		char c = *str;
 
 		if(c == '"' ||
-				(shell_type == ST_NORMAL && (c == '\\' || c == '$' || c == '`')))
+				(shell_type == ST_POSIX && (c == '\\' || c == '$' || c == '`')))
 		{
 			*p++ = '\\';
 		}
@@ -415,6 +423,7 @@ extract_cmd_name(const char line[], int raw, size_t buf_len, char buf[])
 	else
 #endif
 	{
+		/* TODO: this should account for escaping (regular, squotes, dquotes). */
 		result = strchr(line, ' ');
 	}
 	if(result == NULL)
@@ -672,6 +681,43 @@ unichar_isprint(wchar_t ucs)
 	return !unichar_bisearch(ucs, non_printing, ARRAY_LEN(non_printing) - 1);
 }
 
+int
+create_unique_file(char path[], mode_t mode, int auto_delete)
+{
+	/* Probably way too many, but glibc does this much. */
+	enum { MAX_ATTEMPTS = 62*62*62 };
+
+	if(!ends_with(path, "XXXXXX"))
+	{
+		errno = EINVAL;
+		return -1;
+	}
+
+	const char *char_set =
+		"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+	char *suffix = &path[strlen(path) - 6];
+
+	int attempt;
+	for(attempt = 0; attempt < MAX_ATTEMPTS; ++attempt)
+	{
+		int i;
+		for(i = 0; i < 6; ++i)
+		{
+			suffix[i] = char_set[vifm_rand(0, 61)];
+		}
+
+		int fd = create_new_file(path, mode, auto_delete);
+		if(fd != -1 || errno != EEXIST)
+		{
+			return fd;
+		}
+	}
+
+	/* That's what glibc returns in this case. */
+	errno = EEXIST;
+	return -1;
+}
+
 void
 expand_percent_escaping(char s[])
 {
@@ -978,5 +1024,111 @@ unichar_bisearch(wchar_t ucs, const interval_t table[], int max)
 	return 0;
 }
 
+char *
+posix_like_escape(const char string[], int type)
+{
+	size_t len;
+	size_t i;
+	char *ret, *dup;
+
+	len = strlen(string);
+
+	dup = ret = malloc(len*3 + 2 + 1);
+	if(dup == NULL)
+	{
+		return NULL;
+	}
+
+	if(*string == '-')
+	{
+		*dup++ = '.';
+		*dup++ = '/';
+	}
+
+	for(i = 0; i < len; i++, string++, dup++)
+	{
+		switch(*string)
+		{
+			case '%':
+				if(type == 1)
+				{
+					*dup++ = '%';
+				}
+				break;
+
+			/* Escape the following characters anywhere in the line. */
+			case '\'':
+			case '\\':
+			case '\r':
+			case '\t':
+			case '"':
+			case ';':
+			case ' ':
+			case '?':
+			case '|':
+			case '[':
+			case ']':
+			case '{':
+			case '}':
+			case '<':
+			case '>':
+			case '`':
+			case '!':
+			case '$':
+			case '&':
+			case '*':
+			case '(':
+			case ')':
+			case '#':
+				*dup++ = '\\';
+				break;
+
+			case '\n':
+				if(type != 0)
+				{
+					break;
+				}
+
+				*dup++ = '"';
+				*dup++ = '\n';
+				*dup = '"';
+				continue;
+
+			/* Escape the following characters only at the beginning of the line. */
+			case '~':
+			case '=': /* Command-path expansion in zsh. */
+				if(dup == ret)
+					*dup++ = '\\';
+				break;
+		}
+		*dup = *string;
+	}
+	*dup = '\0';
+	return ret;
+}
+
+void
+vifm_srand(unsigned int seed)
+{
+#ifdef USE_POSIX_RANDOM
+	srandom(seed);
+#else
+	srand(seed);
+#endif
+}
+
+int
+vifm_rand(int min, int max)
+{
+	assert(min >= 0 && max >= 0 && min <= max && "Invalid vifm_rand() range.");
+
+#ifdef USE_POSIX_RANDOM
+	double value = random()/(0x7FFFFFFF + 1.0);
+#else
+	double value = rand()/(RAND_MAX + 1.0);
+#endif
+	return min + value*(max - min + 1);
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 filetype=c : */
diff --git a/src/utils/utils.h b/src/utils/utils.h
index bc0d91b..37e4dbd 100644
--- a/src/utils/utils.h
+++ b/src/utils/utils.h
@@ -267,6 +267,16 @@ ExecEnvType get_exec_env_type(void);
 /* Determines kind of the shell by its invocation command.  Returns the kind. */
 ShellType get_shell_type(const char shell_cmd[]);
 
+/* Escapes the string for the purpose of inserting it into a POSIX-like shell or
+ * command-line.  type == 1 enables prepending percent sign with a percent
+ * sign and not escaping newline, because we do only worse.  type == 2 only
+ * skips escaping of newline.  Returns new string, caller should free it. */
+char * posix_like_escape(const char string[], int type);
+
+/* Escapes a string so that it can be used as a command argument in a shell
+ * invocation.  Returns newly allocated string. */
+char * shell_arg_escape(const char what[], ShellType shell_type);
+
 /* Formats command to view documentation in plain-text format.  Returns non-zero
  * if command that should be run in background, otherwise zero is returned. */
 int format_help_cmd(char cmd[], size_t cmd_size);
@@ -313,8 +323,10 @@ FILE * read_cmd_output(const char cmd[], int preserve_stdin);
 const char * get_installed_data_dir(void);
 
 /* Gets path to directory where global configuration files of Vifm are stored.
- * Returns pointer to a statically allocated buffer. */
-const char * get_sys_conf_dir(void);
+ * In case there are multiple such directories, index can be used to enumerate
+ * them starting with 0.  Returns pointer to a statically allocated buffer or
+ * NULL on error or invalid index. */
+const char * get_sys_conf_dir(int idx);
 
 /* Clones attributes from file specified by from to file at path.  st is a hint
  * to omit extra file system requests if possible, can be NULL on Windows. */
@@ -337,6 +349,17 @@ int unichar_bisearch(wchar_t ucs, const interval_t table[], int max);
  * otherwise zero is returned. */
 int unichar_isprint(wchar_t ucs);
 
+/* Creates a temporary file by replacing trailing "XXXXXX" in the path with some
+ * unique sequence.  Updates the path in place.  Returns file descriptor on
+ * success or -1 on failure.  errno is meaningful on failure. */
+int create_unique_file(char path[], mode_t mode, int auto_delete);
+
+/* Initializes random number generator with the seed. */
+void vifm_srand(unsigned int seed);
+
+/* Produces a random number in the ranage [min; max].  Returns the number. */
+int vifm_rand(int min, int max);
+
 #ifdef _WIN32
 #include "utils_win.h"
 #else
diff --git a/src/utils/utils_int.h b/src/utils/utils_int.h
index 4225da4..82b067a 100644
--- a/src/utils/utils_int.h
+++ b/src/utils/utils_int.h
@@ -29,6 +29,10 @@ int run_in_shell_no_cls(char command[], ShellRequester by);
  * Don't pass pipe for input, it can cause deadlock. */
 int run_with_input(char command[], FILE *input, ShellRequester by);
 
+/* Creates a file only if it doesn't exist yet.  Returns file descriptor on
+ * success or -1 on failure.  errno is meaningful on failure. */
+int create_new_file(const char path[], mode_t mode, int auto_delete);
+
 #endif /* VIFM__UTILS__UTILS_INT_H__ */
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/src/utils/utils_nix.c b/src/utils/utils_nix.c
index e699525..b9056db 100644
--- a/src/utils/utils_nix.c
+++ b/src/utils/utils_nix.c
@@ -39,8 +39,8 @@
 #include <grp.h> /* getgrnam() getgrgid_r() */
 #include <pthread.h> /* pthread_sigmask() */
 #include <pwd.h> /* getpwnam() getpwuid_r() */
-#include <unistd.h> /* X_OK chown() dup() dup2() getpid() isatty() pause()
-                       sysconf() ttyname() */
+#include <unistd.h> /* X_OK chown() close() dup() dup2() getpid() isatty()
+                       pause() sysconf() ttyname() */
 
 #include <assert.h> /* assert() */
 #include <ctype.h> /* isdigit() */
@@ -93,6 +93,8 @@ typedef struct
 get_mount_point_traverser_state;
 
 static int get_mount_info_traverser(struct mntent *entry, void *arg);
+static void process_cancel_request(pid_t pid,
+		const cancellation_t *cancellation);
 static void free_mnt_entries(struct mntent *entries, unsigned int nentries);
 static struct mntent * read_mnt_entries(unsigned int *nentries);
 static int clone_mnt_entry(struct mntent *lhs, const struct mntent *rhs);
@@ -168,7 +170,7 @@ run_with_input(char command[], FILE *input, ShellRequester by)
 		_Exit(127);
 	}
 
-	result = get_proc_exit_status(pid);
+	result = get_proc_exit_status(pid, &no_cancellation);
 
 	sigaction(SIGTSTP, &old, NULL);
 
@@ -187,7 +189,7 @@ recover_after_shellout(void)
 
 void
 wait_for_data_from(pid_t pid, FILE *f, int fd,
-		const struct cancellation_t *cancellation)
+		const cancellation_t *cancellation)
 {
 	const struct timeval ts_init = { .tv_sec = 0, .tv_usec = 1000 };
 	struct timeval ts;
@@ -217,8 +219,10 @@ block_all_thread_signals(void)
 	pthread_sigmask(SIG_SETMASK, &set, NULL);
 }
 
-void
-process_cancel_request(pid_t pid, const struct cancellation_t *cancellation)
+/* Checks whether cancelling of current operation is requested and sends SIGINT
+ * to process specified by its process id to request cancellation. */
+static void
+process_cancel_request(pid_t pid, const cancellation_t *cancellation)
 {
 	if(cancellation_requested(cancellation))
 	{
@@ -231,7 +235,7 @@ process_cancel_request(pid_t pid, const struct cancellation_t *cancellation)
 }
 
 int
-get_proc_exit_status(pid_t pid)
+get_proc_exit_status(pid_t pid, const cancellation_t *cancellation)
 {
 	while(1)
 	{
@@ -240,9 +244,11 @@ get_proc_exit_status(pid_t pid)
 		{
 			if(errno == EINTR)
 			{
+				process_cancel_request(pid, cancellation);
 				continue;
 			}
-			LOG_SERROR_MSG(errno, "waitpid()");
+			LOG_SERROR_MSG(errno, "waitpid(%" PRINTF_ULL ") has failed",
+					(unsigned long long)pid);
 			break;
 		}
 
@@ -813,14 +819,21 @@ get_exec_env_type(void)
 ShellType
 get_shell_type(const char shell_cmd[])
 {
-	return ST_NORMAL;
+	return ST_POSIX;
+}
+
+char *
+shell_arg_escape(const char what[], ShellType shell_type)
+{
+	assert(shell_type == ST_POSIX && "Unexpected shell type.");
+	return posix_like_escape(what, /*type=*/0);
 }
 
 int
 format_help_cmd(char cmd[], size_t cmd_size)
 {
 	int bg;
-	char *const escaped = shell_like_escape(cfg.config_dir, 0);
+	char *const escaped = posix_like_escape(cfg.config_dir, /*type=*/0);
 	snprintf(cmd, cmd_size, "%s %s/" VIFM_HELP, cfg_get_vicmd(&bg), escaped);
 	free(escaped);
 	return bg;
@@ -1085,15 +1098,44 @@ get_installed_data_dir(void)
 }
 
 const char *
-get_sys_conf_dir(void)
+get_sys_conf_dir(int idx)
 {
 	static char conf_dir[PATH_MAX + 1];
+	static int init_done;
+
+	if(!init_done)
+	{
+		init_done = 1;
+
+		const char *prefix = env_get("VIFM_APPDIR_ROOT");
+		if(!is_null_or_empty(prefix))
+		{
+			int written = snprintf(conf_dir, sizeof(conf_dir), "%s%s", prefix,
+					PACKAGE_SYSCONF_DIR);
+			if(written < 0 || written >= (int)sizeof(conf_dir))
+			{
+				conf_dir[0] = '\0';
+			}
+		}
+	}
+
 	if(conf_dir[0] == '\0')
 	{
-		const char *prefix = env_get_def("VIFM_APPDIR_ROOT", "");
-		snprintf(conf_dir, sizeof(conf_dir), "%s%s", prefix, PACKAGE_SYSCONF_DIR);
+		/* Offset indices by one to skip AppImage configuration directory which
+		 * isn't present. */
+		++idx;
+	}
+
+	if(idx == 0)
+	{
+		return conf_dir;
+	}
+	if(idx == 1)
+	{
+		return PACKAGE_SYSCONF_DIR;
 	}
-	return conf_dir;
+
+	return NULL;
 }
 
 void
@@ -1216,7 +1258,7 @@ get_drive_info(const char at[], uint64_t *total_bytes, uint64_t *free_bytes)
 }
 
 uint64_t
-get_true_inode(const struct dir_entry_t *entry)
+get_true_inode(const dir_entry_t *entry)
 {
 	if(entry->type != FT_LINK)
 	{
@@ -1234,5 +1276,43 @@ get_true_inode(const struct dir_entry_t *entry)
 	return entry->inode;
 }
 
+void
+bind_pipe_or_die(int fd, int pipe_end, int pipe_other)
+{
+	if(dup2(pipe_end, fd) == -1)
+	{
+		perror("dup2");
+		_Exit(EXIT_FAILURE);
+	}
+
+	/* Close original input pipe descriptors. */
+	if(pipe_end != fd)
+	{
+		close(pipe_end);
+	}
+	close(pipe_other);
+}
+
+int
+create_new_file(const char path[], mode_t mode, int auto_delete)
+{
+	int fd = open(path, O_RDWR | O_CREAT | O_EXCL, mode);
+	if(fd == -1)
+	{
+		return -1;
+	}
+
+	if(auto_delete && unlink(path) != 0)
+	{
+		/* Something weird has happened. */
+		int error = errno;
+		(void)close(fd);
+		errno = error;
+		return -1;
+	}
+
+	return fd;
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 filetype=c : */
diff --git a/src/utils/utils_nix.h b/src/utils/utils_nix.h
index 61ace79..8384b30 100644
--- a/src/utils/utils_nix.h
+++ b/src/utils/utils_nix.h
@@ -31,14 +31,10 @@
 
 struct cancellation_t;
 
-/* Checks whether cancelling of current operation is requested and sends SIGINT
- * to process specified by its process id to request cancellation. */
-void process_cancel_request(pid_t pid,
-		const struct cancellation_t *cancellation);
-
-/* Waits for a process to finish and queries for its exit status.  Returns exit
- * status of the process specified by its identifier. */
-int get_proc_exit_status(pid_t pid);
+/* Waits for a process to finish and queries its exit status.  Cancellation
+ * allows for killing the process by Ctrl+C.  Returns exit status of the
+ * process specified by its identifier or -1 on error. */
+int get_proc_exit_status(pid_t pid, const struct cancellation_t *cancellation);
 
 /* If err_only then use stderr and close stdin and stdout, otherwise both stdout
  * and stderr are redirected to the pipe.  Non-zero preserve_stdin prevents
@@ -78,6 +74,10 @@ int status_to_exit_code(int status);
 
 int S_ISEXE(mode_t mode);
 
+/* Duplicates pipe_end file descriptor to fd and closes pipe_other.  Exists with
+ * an error message on issues. */
+void bind_pipe_or_die(int fd, int pipe_end, int pipe_other);
+
 #endif /* VIFM__UTILS__UTILS_NIX_H__ */
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/src/utils/utils_win.c b/src/utils/utils_win.c
index c1f73f8..7db8a62 100644
--- a/src/utils/utils_win.c
+++ b/src/utils/utils_win.c
@@ -32,6 +32,7 @@
 #include <unistd.h> /* _dup2() _pipe() _spawnvp() close() dup() pipe() */
 
 #include <ctype.h> /* toupper() */
+#include <errno.h> /* EEXIST ENOMEM errno */
 #include <stddef.h> /* NULL size_t */
 #include <stdint.h> /* uint32_t */
 #include <stdlib.h> /* EXIT_SUCCESS free() */
@@ -703,7 +704,13 @@ get_shell_type(const char shell_cmd[])
 	{
 		return ST_PS;
 	}
-	return ST_NORMAL;
+	return ST_POSIX;
+}
+
+char *
+shell_arg_escape(const char what[], ShellType shell_type)
+{
+	return strdup(enclose_in_dquotes(what, shell_type));
 }
 
 int
@@ -746,7 +753,7 @@ update_terminal_settings(void)
 	/* We need ENABLE_WINDOW_INPUT flag to get terminal resize event. */
 	SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), ENABLE_ECHO_INPUT |
 			ENABLE_EXTENDED_FLAGS | ENABLE_INSERT_MODE | ENABLE_LINE_INPUT |
-			ENABLE_MOUSE_INPUT | ENABLE_QUICK_EDIT_MODE | ENABLE_WINDOW_INPUT);
+			ENABLE_MOUSE_INPUT | ENABLE_WINDOW_INPUT);
 }
 
 void
@@ -933,51 +940,9 @@ get_installed_data_dir(void)
 }
 
 const char *
-get_sys_conf_dir(void)
+get_sys_conf_dir(int idx)
 {
-	return get_installed_data_dir();
-}
-
-FILE *
-win_tmpfile(void)
-{
-	char dir[PATH_MAX + 1];
-	char file[PATH_MAX + 1];
-	HANDLE h;
-	int fd;
-	FILE *f;
-
-	if(GetTempPathA(sizeof(dir), dir) == 0)
-	{
-		return NULL;
-	}
-
-	if(GetTempFileNameA(dir, "dir-view", 0U, file) == 0)
-	{
-		return NULL;
-	}
-
-	h = CreateFileA(file, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING,
-			FILE_FLAG_DELETE_ON_CLOSE, NULL);
-	if(h == INVALID_HANDLE_VALUE)
-	{
-		return NULL;
-	}
-
-	fd = _open_osfhandle((intptr_t)h, _O_RDWR);
-	if(fd == -1)
-	{
-		CloseHandle(h);
-		return NULL;
-	}
-
-	f = fdopen(fd, "r+");
-	if(f == NULL)
-	{
-		close(fd);
-	}
-
-	return f;
+	return (idx == 0 ? get_installed_data_dir() : NULL);
 }
 
 void
@@ -1355,5 +1320,27 @@ get_true_inode(const struct dir_entry_t *entry)
 	return 0;
 }
 
+int
+create_new_file(const char path[], mode_t mode, int auto_delete)
+{
+	DWORD attrs = auto_delete ? FILE_FLAG_DELETE_ON_CLOSE : FILE_ATTRIBUTE_NORMAL;
+	HANDLE h = CreateFileA(path, GENERIC_READ | GENERIC_WRITE, 0, NULL,
+			CREATE_NEW, attrs, NULL);
+	if(h == INVALID_HANDLE_VALUE)
+	{
+		errno = EEXIST;
+		return -1;
+	}
+
+	int fd = _open_osfhandle((intptr_t)h, _O_RDWR);
+	if(fd == -1)
+	{
+		CloseHandle(h);
+		errno = ENOMEM;
+	}
+
+	return fd;
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 filetype=c : */
diff --git a/src/utils/utils_win.h b/src/utils/utils_win.h
index bcbb02e..f8b2fe7 100644
--- a/src/utils/utils_win.h
+++ b/src/utils/utils_win.h
@@ -72,12 +72,6 @@ const char * attr_str_long(uint32_t attr);
 /* Returns pointer to a statically allocated buffer. */
 const char * escape_for_cd(const char str[]);
 
-/* tmpfile() for Windows, the way it should have been implemented.  Returns file
- * handler opened for read and write that is automatically removed on
- * application close.  Don't use tmpfile(), they utterly failed to implement
- * it. */
-FILE * win_tmpfile(void);
-
 /* Tries to cancel process gracefully.  Returns zero if cancellation was
  * requested, otherwise non-zero is returned. */
 int win_cancel_process(DWORD pid);
diff --git a/src/vcache.c b/src/vcache.c
index 4565a35..8d84dc7 100644
--- a/src/vcache.c
+++ b/src/vcache.c
@@ -724,9 +724,9 @@ view_external(vcache_entry_t *centry, MacroFlags flags, const char **error)
 		bg_flags |= BJF_SUPPLY_INPUT;
 	}
 
-	if(ma_flags_present(flags, MF_KEEP_SESSION))
+	if(ma_flags_present(flags, MF_KEEP_IN_FG))
 	{
-		bg_flags |= BJF_KEEP_SESSION;
+		bg_flags |= BJF_KEEP_IN_FG;
 	}
 
 	centry->job = bg_run_external_job(centry->viewer, bg_flags);
diff --git a/src/vifm.c b/src/vifm.c
index a7f04c2..5e09a38 100644
--- a/src/vifm.c
+++ b/src/vifm.c
@@ -31,7 +31,7 @@
 #include <locale.h> /* setlocale() LC_ALL */
 #include <stddef.h> /* NULL size_t */
 #include <stdio.h> /* fprintf() fputs() puts() snprintf() */
-#include <stdlib.h> /* EXIT_FAILURE EXIT_SUCCESS exit() srand() system() */
+#include <stdlib.h> /* EXIT_FAILURE EXIT_SUCCESS exit() system() */
 #include <string.h>
 #include <time.h> /* time() */
 
@@ -191,7 +191,7 @@ vifm_main(int argc, char *argv[])
 	}
 
 	(void)setlocale(LC_ALL, "");
-	srand(time(NULL));
+	vifm_srand((unsigned int)time(NULL));
 
 	if(vifm_args.logging)
 	{
@@ -218,7 +218,7 @@ vifm_main(int argc, char *argv[])
 	reinit_logger(cfg.log_file);
 
 	/* Commands module also initializes bracket notation and variables. */
-	init_commands();
+	cmds_init();
 
 	init_builtin_functions();
 	update_path_env(1);
@@ -257,7 +257,16 @@ vifm_main(int argc, char *argv[])
 
 	args_process(&vifm_args, AS_OTHER, curr_stats.ipc);
 
-	bg_init();
+	/* $VIFM/plugins is to be searched for plugins last. */
+	curr_stats.plugins_dirs.nitems = put_into_string_array(
+			&curr_stats.plugins_dirs.items, curr_stats.plugins_dirs.nitems,
+			format_str("%s/plugins", cfg.config_dir));
+
+	if(bg_init() != 0)
+	{
+		fprintf(stderr, "Failed to initialize threads.\n");
+		return -1;
+	}
 
 	fops_init(&modcline_prompt, &prompt_msg_custom);
 
@@ -292,7 +301,7 @@ vifm_main(int argc, char *argv[])
 		return -1;
 	}
 
-	init_modes();
+	modes_init();
 	un_init(&undo_perform_func, NULL, &ui_cancellation_requested,
 			&cfg.undo_levels);
 	load_view_options(curr_view);
@@ -324,7 +333,7 @@ vifm_main(int argc, char *argv[])
 		(void)trash_set_specs(cfg.trash_dir);
 	}
 
-	plugs_load(curr_stats.plugs, cfg.config_dir);
+	plugs_load(curr_stats.plugs, curr_stats.plugins_dirs);
 
 	check_path_for_file(&lwin, vifm_args.lwin_path, vifm_args.lwin_handle);
 	check_path_for_file(&rwin, vifm_args.rwin_path, vifm_args.rwin_handle);
@@ -406,9 +415,10 @@ parse_received_arguments(char *argv[])
 	opterr = 0;
 	args_parse(&args, count_strings(argv), argv, argv[0]);
 	args_process(&args, AS_IPC, curr_stats.ipc);
+	/* XXX: why AS_OTHER invocation is used with IPC args, is this a mistake? */
 	args_process(&args, AS_OTHER, curr_stats.ipc);
 
-	abort_menu_like_mode();
+	modes_abort_menu_like();
 	exec_startup_commands(&args);
 	update_screen(stats_update_fetch());
 
@@ -510,16 +520,15 @@ need_to_switch_active_pane(const char lwin_path[], const char rwin_path[])
 static char *
 eval_received_expression(const char expr[])
 {
-	char *result_str;
-
-	var_t result;
-	if(parse(expr, 1, &result) != PE_NO_ERROR)
+	parsing_result_t result = vle_parser_eval(expr, /*interactive=*/1);
+	if(result.error != PE_NO_ERROR)
 	{
+		var_free(result.value);
 		return NULL;
 	}
 
-	result_str = var_to_str(result);
-	var_free(result);
+	char *result_str = var_to_str(result.value);
+	var_free(result.value);
 	return result_str;
 }
 
@@ -555,7 +564,7 @@ exec_startup_commands(const args_t *args)
 		/* Make sure we're executing commands in correct directory. */
 		(void)vifm_chdir(flist_get_dir(curr_view));
 
-		(void)exec_commands(args->cmds[i], curr_view, CIT_COMMAND);
+		(void)cmds_dispatch(args->cmds[i], curr_view, CIT_COMMAND);
 	}
 }
 
@@ -571,6 +580,10 @@ vifm_try_leave(int store_state, int cquit, int force)
 		}
 	}
 
+	/* Has to be run early when UI is up and state can still be changed. */
+	vlua_events_app_exit(curr_stats.vlua);
+	vlua_process_callbacks(curr_stats.vlua);
+
 	fuse_unmount_all();
 
 	if(store_state)
@@ -594,6 +607,10 @@ vifm_try_leave(int store_state, int cquit, int force)
 void _gnuc_noreturn
 vifm_choose_files(view_t *view, int nfiles, char *files[])
 {
+	/* Has to be run early when UI is up and state can still be changed. */
+	vlua_events_app_exit(curr_stats.vlua);
+	vlua_process_callbacks(curr_stats.vlua);
+
 	/* As curses can do something with terminal on shutting down, disable it
 	 * before writing anything to the screen. */
 	ui_shutdown();
diff --git a/tests/Makefile b/tests/Makefile
index 84efbf4..575e9b1 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -41,9 +41,14 @@
 # "B" variable might be set to build tree root to run tests out of the source
 # tree.
 
-# don't pass $TERM to tests, they should work without a terminal and this
-# variable
-unexport TERM
+# don't undefine $TERM if we're going to invoke gdb or it will prevent its TUI
+# from starting
+ifneq ($(DEBUG),gdb)
+    # don't pass $TERM to tests, they should work without a terminal and this
+    # variable
+    unexport TERM
+endif
+
 # hide terminal multiplexers and graphic systems, so tests behave consistently
 unexport STY TMUX DISPLAY WAYLAND_DISPLAY
 
@@ -69,6 +74,14 @@ ifdef DEBUG
     BINSUBDIR := debug/
 endif
 
+# test for a bad directory path which breaks the Makefile
+ifndef win_env
+    # testing for # seems to work only for some versions of GNU Make
+    ifneq (,$(findstring :,$(PWD)))
+        $(error Paths with : break tests' Makefile)
+    endif
+endif
+
 # path to build tree
 B ?=
 # path to storage for intermediate build files
@@ -82,7 +95,7 @@ suites += ioeta ionotif iop ior
 # ui
 suites += colmgr column_view viewcolumns_parser
 # everything else
-suites += bmarks env escape fileops filetype filter lua misc undo utils
+suites += bmarks env escape fileops filetype filter lua menus misc undo utils
 
 # these are built, but not automatically executed
 apps := fuzz regs_shmem_app
@@ -94,7 +107,7 @@ vifm_src := $(wildcard $(addprefix ../src/, $(addsuffix *.c, $(vifm_src))))
 vifm_src := $(filter-out %/tags.c %/xxhash.c, $(vifm_src))
 
 # filter out generally non-testable or sources for another platform
-vifm_src := $(filter-out %/vifm.c %/win_helper.c, $(vifm_src))
+vifm_src := $(filter-out %/src/./vifm.c %/win_helper.c, $(vifm_src))
 ifndef unix_env
     vifm_src := $(filter-out %/desktop.c %/media_menu.c %/mntent.c %_nix.c, \
                              $(vifm_src))
diff --git a/tests/cmds/builtin.c b/tests/cmds/builtin.c
index ce69d32..7defa23 100644
--- a/tests/cmds/builtin.c
+++ b/tests/cmds/builtin.c
@@ -13,7 +13,7 @@ extern struct cmds_conf cmds_conf;
 SETUP()
 {
 	vle_cmds_reset();
-	init_commands();
+	cmds_init();
 }
 
 TEST(empty_line_completion)
diff --git a/tests/commands/autocd.c b/tests/commands/autocd.c
index d2bc8ae..555ad0b 100644
--- a/tests/commands/autocd.c
+++ b/tests/commands/autocd.c
@@ -28,7 +28,7 @@ SETUP()
 
 	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), TEST_DATA_PATH, "", NULL);
 
-	init_commands();
+	cmds_init();
 
 	cfg.auto_cd = 1;
 }
@@ -48,25 +48,25 @@ TEARDOWN()
 
 TEST(existing_dir)
 {
-	assert_success(exec_command("read", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch1("read", &lwin, CIT_COMMAND));
 	assert_true(paths_are_equal(lwin.curr_dir, read_path));
 }
 
 TEST(parent_dir)
 {
-	assert_success(exec_command("read", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch1("read", &lwin, CIT_COMMAND));
 	assert_true(paths_are_equal(lwin.curr_dir, read_path));
 
-	assert_success(exec_command("..", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch1("..", &lwin, CIT_COMMAND));
 	assert_true(paths_are_equal(lwin.curr_dir, test_data));
 
-	assert_success(exec_command("read", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch1("read", &lwin, CIT_COMMAND));
 	assert_true(paths_are_equal(lwin.curr_dir, read_path));
 }
 
 TEST(ambiguity_and_error)
 {
-	assert_failure(exec_command("compare/a", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch1("compare/a", &lwin, CIT_COMMAND));
 	assert_true(paths_are_equal(lwin.curr_dir, test_data));
 }
 
diff --git a/tests/commands/autocmds.c b/tests/commands/autocmds.c
index 3b82401..c818fb1 100644
--- a/tests/commands/autocmds.c
+++ b/tests/commands/autocmds.c
@@ -39,7 +39,7 @@ SETUP()
 
 	update_string(&cfg.slow_fs_list, "");
 
-	init_commands();
+	cmds_init();
 	opt_handlers_setup();
 
 	view_setup(&lwin);
@@ -60,21 +60,21 @@ TEARDOWN()
 
 TEST(no_args_lists_elements)
 {
-	assert_failure(exec_commands("autocmd", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("autocmd", &lwin, CIT_COMMAND));
 }
 
 TEST(addition_start)
 {
 	snprintf(cmd, sizeof(cmd), "autocmd * '%s' let $a = 1", sandbox);
-	assert_failure(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 }
 
 TEST(addition_match)
 {
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
 
 	snprintf(cmd, sizeof(cmd), "autocmd DirEnter '%s' let $a = 1", sandbox);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 
 	assert_string_equal("x", env_get("a"));
 	assert_true(change_view_directory(curr_view, sandbox) >= 0);
@@ -83,11 +83,11 @@ TEST(addition_match)
 
 TEST(autocmd_is_whole_line_command)
 {
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
 
 	snprintf(cmd, sizeof(cmd), "autocmd DirEnter '%s' let $a = 1 | let $a = 2",
 			sandbox);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 
 	assert_string_equal("x", env_get("a"));
 	assert_true(change_view_directory(curr_view, sandbox) >= 0);
@@ -96,10 +96,10 @@ TEST(autocmd_is_whole_line_command)
 
 TEST(addition_no_match)
 {
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
 
 	snprintf(cmd, sizeof(cmd), "autocmd DirEnter '%s' let $a = 1", sandbox);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 
 	assert_string_equal("x", env_get("a"));
 	assert_true(change_view_directory(curr_view, test_data) >= 0);
@@ -108,12 +108,12 @@ TEST(addition_no_match)
 
 TEST(remove_exact_match)
 {
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
 
 	snprintf(cmd, sizeof(cmd), "autocmd DirEnter '%s' let $a = 1", sandbox);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 	snprintf(cmd, sizeof(cmd), "autocmd! DirEnter '%s'", sandbox);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 
 	assert_string_equal("x", env_get("a"));
 	assert_true(change_view_directory(curr_view, sandbox) >= 0);
@@ -122,11 +122,11 @@ TEST(remove_exact_match)
 
 TEST(remove_event_match)
 {
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
 
 	snprintf(cmd, sizeof(cmd), "autocmd DirEnter '%s' let $a = 1", sandbox);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
-	assert_success(exec_commands("autocmd! DirEnter", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("autocmd! DirEnter", &lwin, CIT_COMMAND));
 
 	assert_string_equal("x", env_get("a"));
 	assert_true(change_view_directory(curr_view, sandbox) >= 0);
@@ -135,12 +135,12 @@ TEST(remove_event_match)
 
 TEST(remove_path_match)
 {
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
 
 	snprintf(cmd, sizeof(cmd), "autocmd DirEnter '%s' let $a = 1", sandbox);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 	snprintf(cmd, sizeof(cmd), "autocmd! * '%s'", sandbox);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 
 	assert_string_equal("x", env_get("a"));
 	assert_true(change_view_directory(curr_view, sandbox) >= 0);
@@ -149,11 +149,11 @@ TEST(remove_path_match)
 
 TEST(remove_all)
 {
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
 
 	snprintf(cmd, sizeof(cmd), "autocmd DirEnter '%s' let $a = 1", sandbox);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
-	assert_success(exec_commands("autocmd!", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("autocmd!", &lwin, CIT_COMMAND));
 
 	assert_string_equal("x", env_get("a"));
 	assert_true(change_view_directory(curr_view, sandbox) >= 0);
@@ -162,15 +162,15 @@ TEST(remove_all)
 
 TEST(remove_too_many_args)
 {
-	assert_failure(exec_commands("autocmd! a b c", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("autocmd! a b c", &lwin, CIT_COMMAND));
 }
 
 TEST(extra_slash_is_fine)
 {
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
 
 	snprintf(cmd, sizeof(cmd), "auto DirEnter '%s/' let $a = 1", sandbox);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 
 	assert_string_equal("x", env_get("a"));
 	assert_true(change_view_directory(curr_view, sandbox) >= 0);
@@ -179,7 +179,7 @@ TEST(extra_slash_is_fine)
 
 TEST(error_on_wrong_event_name)
 {
-	assert_failure(exec_commands("autocmd some /path let $a = 1", &lwin,
+	assert_failure(cmds_dispatch("autocmd some /path let $a = 1", &lwin,
 				CIT_COMMAND));
 }
 
@@ -187,11 +187,11 @@ TEST(envvars_are_expanded)
 {
 	char cmd[PATH_MAX + 1];
 
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
 
 	snprintf(cmd, sizeof(cmd), "let $dir = '%s'", sandbox);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
-	assert_success(exec_commands("autocmd DirEnter $dir let $a = 1", &lwin,
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("autocmd DirEnter $dir let $a = 1", &lwin,
 				CIT_COMMAND));
 
 	assert_string_equal("x", env_get("a"));
@@ -201,10 +201,10 @@ TEST(envvars_are_expanded)
 
 TEST(pattern_negation)
 {
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
 
 	snprintf(cmd, sizeof(cmd), "auto DirEnter '!%s' let $a = 1", sandbox);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 
 	assert_string_equal("x", env_get("a"));
 	assert_true(change_view_directory(curr_view, sandbox) >= 0);
@@ -215,9 +215,9 @@ TEST(pattern_negation)
 
 TEST(tail_pattern)
 {
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
 
-	assert_success(exec_commands("auto DirEnter existing-files let $a = 1", &lwin,
+	assert_success(cmds_dispatch("auto DirEnter existing-files let $a = 1", &lwin,
 				CIT_COMMAND));
 
 	assert_string_equal("x", env_get("a"));
@@ -229,10 +229,10 @@ TEST(tail_pattern)
 
 TEST(multiple_patterns_addition)
 {
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
 
 	snprintf(cmd, sizeof(cmd), "auto DirEnter '%s,ab' let $a = 1", sandbox);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 
 	assert_string_equal("x", env_get("a"));
 	assert_true(change_view_directory(curr_view, sandbox) >= 0);
@@ -241,13 +241,13 @@ TEST(multiple_patterns_addition)
 
 TEST(multiple_patterns_removal)
 {
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
 
 	snprintf(cmd, sizeof(cmd), "auto DirEnter '%s,%s' let $a = 1", sandbox,
 			test_data);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 	snprintf(cmd, sizeof(cmd), "auto! DirEnter '%s,%s'", sandbox, test_data);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 
 	assert_string_equal("x", env_get("a"));
 	assert_true(change_view_directory(curr_view, sandbox) >= 0);
@@ -260,12 +260,12 @@ TEST(multiple_patterns_correct_expansion)
 	/* Each pattern should be expanded on its own, not all pattern string should
 	 * be expanded and then broken into patterns. */
 
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("let $c = ','", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $c = ','", &lwin, CIT_COMMAND));
 
 	snprintf(cmd, sizeof(cmd), "auto DirEnter '%s$c%s' let $a = 1", sandbox,
 			test_data);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 
 	assert_string_equal("x", env_get("a"));
 	assert_true(change_view_directory(curr_view, sandbox) >= 0);
@@ -274,14 +274,14 @@ TEST(multiple_patterns_correct_expansion)
 
 TEST(direnter_is_not_triggered_on_leaving_custom_view_to_original_path)
 {
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
 
 	assert_true(change_view_directory(curr_view, sandbox) >= 0);
 	replace_string(&curr_view->custom.orig_dir, curr_view->curr_dir);
 	curr_view->curr_dir[0] = '\0';
 
 	snprintf(cmd, sizeof(cmd), "auto DirEnter '%s' let $a = 1", sandbox);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 
 	assert_string_equal("x", env_get("a"));
 	assert_true(change_view_directory(curr_view, sandbox) >= 0);
@@ -293,11 +293,11 @@ TEST(direnter_can_be_triggered_on_entering_custom_view_to_different_path)
 	cfg.cvoptions = CVO_AUTOCMDS;
 
 	snprintf(cmd, sizeof(cmd), "auto DirEnter '%s' let $a = 1", sandbox);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 
 	assert_true(change_view_directory(curr_view, sandbox) >= 0);
 
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
 	assert_string_equal("x", env_get("a"));
 
 	char path[PATH_MAX + 1];
@@ -315,14 +315,14 @@ TEST(direnter_can_be_triggered_on_entering_custom_view_to_different_path)
 
 TEST(direnter_is_triggered_on_leaving_custom_view_to_different_path)
 {
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
 
 	assert_true(change_view_directory(curr_view, sandbox) >= 0);
 	replace_string(&curr_view->custom.orig_dir, curr_view->curr_dir);
 	curr_view->curr_dir[0] = '\0';
 
 	snprintf(cmd, sizeof(cmd), "auto DirEnter '%s' let $a = 1", test_data);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 
 	assert_string_equal("x", env_get("a"));
 	assert_true(change_view_directory(curr_view, test_data) >= 0);
@@ -333,7 +333,7 @@ TEST(autocmd_in_vifmrc_affects_only_current_view)
 {
 	snprintf(cmd, sizeof(cmd), "auto DirEnter '%s' setlocal nodotfiles",
 			test_data);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 
 	lwin.hide_dot = 0;
 	rwin.hide_dot = 0;
@@ -352,11 +352,11 @@ TEST(tilde_is_expanded_after_negation, IF(not_windows))
 	char path[PATH_MAX + 1];
 	snprintf(path, sizeof(path), "%s/~", sandbox);
 
-	assert_success(exec_commands("let $a = 'x'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND));
 
 	assert_success(os_mkdir(path, 0700));
 
-	assert_success(exec_commands("auto DirEnter !~ let $a = 1", &lwin,
+	assert_success(cmds_dispatch("auto DirEnter !~ let $a = 1", &lwin,
 				CIT_COMMAND));
 
 	assert_string_equal("x", env_get("a"));
diff --git a/tests/commands/bmarks.c b/tests/commands/bmarks.c
index 5164112..c999021 100644
--- a/tests/commands/bmarks.c
+++ b/tests/commands/bmarks.c
@@ -18,7 +18,7 @@ static int cb_called;
 
 SETUP()
 {
-	init_commands();
+	cmds_init();
 	lwin.selected_files = 0;
 	strcpy(lwin.curr_dir, "/a/path");
 	path = NULL;
@@ -30,102 +30,112 @@ SETUP()
 
 TEARDOWN()
 {
-	assert_success(exec_commands("delbmarks!", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("delbmarks!", &lwin, CIT_COMMAND));
 	free(path);
 	free(tags);
 }
 
 TEST(tag_with_comma_is_rejected)
 {
-	assert_failure(exec_commands("bmark a,b", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("bmark a,b", &lwin, CIT_COMMAND));
 }
 
 TEST(tag_with_space_is_rejected)
 {
-	assert_failure(exec_commands("bmark a\\ b", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("bmark a\\ b", &lwin, CIT_COMMAND));
+}
+
+TEST(emark_with_bookmark_path_only)
+{
+	assert_failure(cmds_dispatch("bmark! /fake/path", &lwin, CIT_COMMAND));
 }
 
 TEST(emark_allows_specifying_bookmark_path)
 {
-	assert_success(exec_commands("bmark! /fake/path tag", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /fake/path tag", &lwin, CIT_COMMAND));
 	assert_int_equal(1, count_bmarks());
 }
 
 TEST(delbmarks_with_emark_removes_all_tags)
 {
-	assert_success(exec_commands("bmark! /path1 tag1", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmark! /path2 tag2", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmark! /path3 tag3", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /path1 tag1", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /path2 tag2", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /path3 tag3", &lwin, CIT_COMMAND));
 
-	assert_success(exec_commands("delbmarks!", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("delbmarks!", &lwin, CIT_COMMAND));
 	assert_int_equal(0, count_bmarks());
 }
 
 TEST(delbmarks_with_emark_removes_selected_bookmarks)
 {
-	assert_success(exec_commands("bmark! /path1 tag1", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmark! /path2 tag2", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmark! /path3 tag3", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /path1 tag1", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /path2 tag2", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /path3 tag3", &lwin, CIT_COMMAND));
 
-	assert_success(exec_commands("delbmarks! /path1 /path3", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("delbmarks! /path1 /path3", &lwin, CIT_COMMAND));
 	assert_int_equal(1, count_bmarks());
 }
 
 TEST(delbmarks_without_args_removes_current_mark)
 {
-	assert_success(exec_commands("bmark tag1", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("delbmarks", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark tag1", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("delbmarks", &lwin, CIT_COMMAND));
 	assert_int_equal(0, count_bmarks());
 }
 
 TEST(delbmarks_with_args_removes_matching_bookmarks)
 {
-	assert_success(exec_commands("bmark! /path1 t1 t2", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmark! /path2 t2 t3", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmark! /path3 t1 t3", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /path1 t1 t2", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /path2 t2 t3", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /path3 t1 t3", &lwin, CIT_COMMAND));
 
-	assert_success(exec_commands("delbmarks t3", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("delbmarks t3", &lwin, CIT_COMMAND));
 	assert_int_equal(1, count_bmarks());
 }
 
+TEST(delbmarks_tag_with_comma_is_rejected)
+{
+	assert_failure(cmds_dispatch("delbmarks a,b", &lwin, CIT_COMMAND));
+}
+
 TEST(arguments_are_unescaped)
 {
-	assert_success(exec_commands("bmark! /\\*stars\\* tag", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /\\*stars\\* tag", &lwin, CIT_COMMAND));
 	assert_int_equal(1, count_bmarks());
 	assert_string_equal("/*stars*", path);
 }
 
 TEST(arguments_are_unquoted_single)
 {
-	assert_success(exec_commands("bmark! '/squotes' tag", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! '/squotes' tag", &lwin, CIT_COMMAND));
 	assert_int_equal(1, count_bmarks());
 	assert_string_equal("/squotes", path);
 }
 
 TEST(arguments_are_unquoted_double)
 {
-	assert_success(exec_commands("bmark! \"/dquotes\" tag", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! \"/dquotes\" tag", &lwin, CIT_COMMAND));
 	assert_int_equal(1, count_bmarks());
 	assert_string_equal("/dquotes", path);
 }
 
 TEST(first_argument_is_expanded)
 {
-	assert_success(exec_commands("bmark! %d tag", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! %d tag", &lwin, CIT_COMMAND));
 	assert_int_equal(1, count_bmarks());
 	assert_string_equal("/a/path", path);
 }
 
 TEST(not_first_argument_is_not_expanded)
 {
-	assert_success(exec_commands("bmark! /dir %d", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /dir %d", &lwin, CIT_COMMAND));
 	assert_int_equal(1, count_bmarks());
 	assert_string_equal("%d", tags);
 }
 
 TEST(not_all_macros_are_expanded)
 {
-	assert_success(exec_commands("bmark! /%b%n%i%a%m%M%s%S%u%U%px tag", &lwin,
+	assert_success(cmds_dispatch("bmark! /%b%n%i%a%m%M%s%S%u%U%px tag", &lwin,
 				CIT_COMMAND));
 	assert_int_equal(1, count_bmarks());
 	assert_string_equal("/", path);
@@ -133,7 +143,7 @@ TEST(not_all_macros_are_expanded)
 
 TEST(tilde_is_expanded)
 {
-	assert_success(exec_commands("bmark! ~ tag", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! ~ tag", &lwin, CIT_COMMAND));
 	assert_int_equal(1, count_bmarks());
 	assert_false(path[0] == '~');
 	assert_false(path[strlen(path) - 1] == '~');
diff --git a/tests/commands/cabbrev.c b/tests/commands/cabbrev.c
index 2abeaea..03cb6ca 100644
--- a/tests/commands/cabbrev.c
+++ b/tests/commands/cabbrev.c
@@ -5,6 +5,7 @@
 #include "../../src/cfg/config.h"
 #include "../../src/engine/abbrevs.h"
 #include "../../src/engine/cmds.h"
+#include "../../src/ui/statusbar.h"
 #include "../../src/ui/ui.h"
 #include "../../src/cmd_core.h"
 
@@ -21,7 +22,7 @@ SETUP()
 
 	curr_view = &lwin;
 
-	init_commands();
+	cmds_init();
 }
 
 TEARDOWN()
@@ -34,10 +35,10 @@ TEST(addition)
 {
 	int no_remap;
 
-	assert_success(exec_commands("cabbrev lhs1 rhs1", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cabbrev lhs1 rhs1", &lwin, CIT_COMMAND));
 	assert_wstring_equal(L"rhs1", vle_abbr_expand(L"lhs1", &no_remap));
 
-	assert_success(exec_commands("cnoreabbrev lhs2 rhs2", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cnoreabbrev lhs2 rhs2", &lwin, CIT_COMMAND));
 	assert_wstring_equal(L"rhs2", vle_abbr_expand(L"lhs2", &no_remap));
 }
 
@@ -45,10 +46,10 @@ TEST(single_character_abbrev)
 {
 	int no_remap;
 
-	assert_success(exec_commands("cabbrev x y", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cabbrev x y", &lwin, CIT_COMMAND));
 	assert_wstring_equal(L"y", vle_abbr_expand(L"x", &no_remap));
 
-	assert_success(exec_commands("cnoreabbrev a b", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cnoreabbrev a b", &lwin, CIT_COMMAND));
 	assert_wstring_equal(L"b", vle_abbr_expand(L"a", &no_remap));
 }
 
@@ -56,7 +57,7 @@ TEST(add_with_remap)
 {
 	int no_remap = -1;
 
-	assert_success(exec_commands("cabbrev lhs rhs", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cabbrev lhs rhs", &lwin, CIT_COMMAND));
 	assert_non_null(vle_abbr_expand(L"lhs", &no_remap));
 	assert_false(no_remap);
 }
@@ -65,7 +66,7 @@ TEST(add_with_no_remap)
 {
 	int no_remap = -1;
 
-	assert_success(exec_commands("cnoreabbrev lhs rhs", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cnoreabbrev lhs rhs", &lwin, CIT_COMMAND));
 	assert_non_null(vle_abbr_expand(L"lhs", &no_remap));
 	assert_true(no_remap);
 }
@@ -74,30 +75,30 @@ TEST(double_cabbrev_same_lhs)
 {
 	int no_remap = -1;
 
-	assert_success(exec_commands("cabbrev lhs rhs1", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cabbrev lhs rhs1", &lwin, CIT_COMMAND));
 	assert_wstring_equal(L"rhs1", vle_abbr_expand(L"lhs", &no_remap));
 	assert_false(no_remap);
 
-	assert_success(exec_commands("cabbrev lhs rhs2", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cabbrev lhs rhs2", &lwin, CIT_COMMAND));
 	assert_wstring_equal(L"rhs2", vle_abbr_expand(L"lhs", &no_remap));
 	assert_false(no_remap);
 
-	assert_success(exec_commands("cnoreabbrev lhs rhs1", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("cnoreabbrev lhs rhs2", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cnoreabbrev lhs rhs1", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cnoreabbrev lhs rhs2", &lwin, CIT_COMMAND));
 }
 
 TEST(unabbrev_by_lhs)
 {
 	int no_remap;
 
-	assert_success(exec_commands("cabbrev lhs rhs", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cabbrev lhs rhs", &lwin, CIT_COMMAND));
 	assert_non_null(vle_abbr_expand(L"lhs", &no_remap));
-	assert_success(exec_commands("cunabbrev lhs", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cunabbrev lhs", &lwin, CIT_COMMAND));
 	assert_null(vle_abbr_expand(L"lhs", &no_remap));
 
-	assert_success(exec_commands("cnoreabbrev lhs rhs", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cnoreabbrev lhs rhs", &lwin, CIT_COMMAND));
 	assert_non_null(vle_abbr_expand(L"lhs", &no_remap));
-	assert_success(exec_commands("cunabbrev lhs", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cunabbrev lhs", &lwin, CIT_COMMAND));
 	assert_null(vle_abbr_expand(L"lhs", &no_remap));
 }
 
@@ -105,16 +106,26 @@ TEST(unabbrev_by_rhs)
 {
 	int no_remap;
 
-	assert_success(exec_commands("cabbrev lhs rhs", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cabbrev lhs rhs", &lwin, CIT_COMMAND));
 	assert_non_null(vle_abbr_expand(L"lhs", &no_remap));
-	assert_success(exec_commands("cunabbrev rhs", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cunabbrev rhs", &lwin, CIT_COMMAND));
 	assert_null(vle_abbr_expand(L"lhs", &no_remap));
 
-	assert_success(exec_commands("cnoreabbrev lhs rhs", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cnoreabbrev lhs rhs", &lwin, CIT_COMMAND));
 	assert_non_null(vle_abbr_expand(L"lhs", &no_remap));
-	assert_success(exec_commands("cunabbrev rhs", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cunabbrev rhs", &lwin, CIT_COMMAND));
 	assert_null(vle_abbr_expand(L"lhs", &no_remap));
 }
 
+TEST(unabbrev_error)
+{
+	int no_remap;
+
+	ui_sb_msg("");
+	assert_failure(cmds_dispatch("cunabbrev rhs", &lwin, CIT_COMMAND));
+	assert_null(vle_abbr_expand(L"lhs", &no_remap));
+	assert_string_equal("No such abbreviation: rhs", ui_sb_last());
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/commands/cds.c b/tests/commands/cds.c
index 4bd231f..67a1791 100644
--- a/tests/commands/cds.c
+++ b/tests/commands/cds.c
@@ -22,7 +22,7 @@ SETUP()
 	curr_view = &lwin;
 	other_view = &rwin;
 
-	init_commands();
+	cmds_init();
 
 	conf_setup();
 }
@@ -54,7 +54,7 @@ TEST(cds_does_the_replacement)
 	assert_success(chdir(path));
 	strcpy(lwin.curr_dir, path);
 
-	assert_success(exec_commands("cds syntax-highlight rename", &lwin,
+	assert_success(cmds_dispatch("cds syntax-highlight rename", &lwin,
 				CIT_COMMAND));
 
 	snprintf(path, sizeof(path), "%s/rename", test_data);
@@ -70,7 +70,7 @@ TEST(cds_aborts_on_broken_)
 
 	strcpy(lwin.curr_dir, dst);
 
-	assert_failure(exec_commands("cds/rename/read/t", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("cds/rename/read/t", &lwin, CIT_COMMAND));
 
 	assert_string_equal(dst, lwin.curr_dir);
 }
@@ -82,7 +82,7 @@ TEST(cds_acts_like_substitute)
 	assert_success(chdir(path));
 	strcpy(lwin.curr_dir, path);
 
-	assert_success(exec_commands("cds/SYNtax-?hi[a-z]*/rename/i", &lwin,
+	assert_success(cmds_dispatch("cds/SYNtax-?hi[a-z]*/rename/i", &lwin,
 				CIT_COMMAND));
 
 	snprintf(path, sizeof(path), "%s/rename", test_data);
@@ -96,7 +96,7 @@ TEST(cds_can_change_path_of_both_panes)
 	assert_success(chdir(path));
 	strcpy(lwin.curr_dir, path);
 
-	assert_success(exec_commands("cds! syntax-highlight rename", &lwin,
+	assert_success(cmds_dispatch("cds! syntax-highlight rename", &lwin,
 				CIT_COMMAND));
 
 	snprintf(path, sizeof(path), "%s/rename", test_data);
@@ -111,7 +111,7 @@ TEST(cds_is_noop_when_pattern_not_found)
 	strcpy(lwin.curr_dir, test_data);
 	strcpy(rwin.curr_dir, sandbox);
 
-	assert_failure(exec_commands("cds asdlfkjasdlkfj rename", &lwin,
+	assert_failure(cmds_dispatch("cds asdlfkjasdlkfj rename", &lwin,
 				CIT_COMMAND));
 
 	assert_string_equal(test_data, lwin.curr_dir);
diff --git a/tests/commands/colorscheme.c b/tests/commands/colorscheme.c
index 178a76f..8ee8891 100644
--- a/tests/commands/colorscheme.c
+++ b/tests/commands/colorscheme.c
@@ -21,7 +21,7 @@ SETUP()
 	curr_view = &lwin;
 	other_view = &rwin;
 
-	init_commands();
+	cmds_init();
 
 	saved_cwd = save_cwd();
 	cs_load_defaults();
@@ -43,7 +43,7 @@ TEST(current_colorscheme_is_printed)
 	strcpy(cfg.cs.name, "test-scheme");
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("colorscheme?", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("colorscheme?", &lwin, CIT_COMMAND));
 	assert_string_equal("test-scheme", ui_sb_last());
 }
 
@@ -54,7 +54,7 @@ TEST(unknown_colorscheme_is_not_loaded)
 	cfg.cs.color[WIN_COLOR].bg = 2;
 	cfg.cs.color[WIN_COLOR].attr = 3;
 
-	assert_failure(exec_commands("colorscheme bad-cs-name", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("colorscheme bad-cs-name", &lwin, CIT_COMMAND));
 
 	assert_string_equal("test-scheme", cfg.cs.name);
 	assert_int_equal(1, cfg.cs.color[WIN_COLOR].fg);
@@ -76,7 +76,7 @@ TEST(good_colorscheme_is_loaded)
 	cfg.cs.color[WIN_COLOR].bg = 2;
 	cfg.cs.color[WIN_COLOR].attr = 3;
 
-	assert_success(exec_commands("colorscheme good", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("colorscheme good", &lwin, CIT_COMMAND));
 
 	assert_string_equal("good", cfg.cs.name);
 	assert_int_equal(-1, cfg.cs.color[WIN_COLOR].fg);
@@ -86,7 +86,7 @@ TEST(good_colorscheme_is_loaded)
 
 TEST(unknown_colorscheme_is_not_associated)
 {
-	assert_failure(exec_commands("colorscheme bad-name /", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("colorscheme bad-name /", &lwin, CIT_COMMAND));
 }
 
 TEST(colorscheme_is_not_associated_to_a_file)
@@ -98,17 +98,17 @@ TEST(colorscheme_is_not_associated_to_a_file)
 	char cmd[PATH_MAX + 1];
 	snprintf(cmd, sizeof(cmd), "colorscheme good %s", path);
 
-	assert_failure(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 }
 
 TEST(colorscheme_is_not_associated_to_relpath_on_startup)
 {
 	strcpy(lwin.curr_dir, saved_cwd);
 
-	assert_success(exec_commands("colorscheme good .", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("colorscheme good .", &lwin, CIT_COMMAND));
 	assert_false(cs_load_local(1, "."));
 
-	assert_success(exec_commands("colorscheme name1 name2", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("colorscheme name1 name2", &lwin, CIT_COMMAND));
 	assert_false(cs_load_local(1, "name2"));
 }
 
@@ -122,7 +122,7 @@ TEST(colorscheme_is_associated_to_tildepath_on_startup)
 	create_dir(SANDBOX_PATH "/colors");
 	create_file(SANDBOX_PATH "/colors/cs.vifm");
 
-	assert_success(exec_commands("colorscheme cs ~/colors", &lwin,
+	assert_success(cmds_dispatch("colorscheme cs ~/colors", &lwin,
 				CIT_COMMAND));
 	char *path = expand_tilde("~/colors");
 	assert_true(cs_load_local(1, path));
@@ -139,7 +139,7 @@ TEST(colorscheme_is_restored_on_bad_name)
 	cfg.cs.color[WIN_COLOR].bg = 2;
 	cfg.cs.color[WIN_COLOR].attr = 3;
 
-	assert_success(exec_commands("colorscheme bad-color", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("colorscheme bad-color", &lwin, CIT_COMMAND));
 
 	assert_string_equal("test-scheme", cfg.cs.name);
 	assert_int_equal(1, cfg.cs.color[WIN_COLOR].fg);
@@ -156,7 +156,7 @@ TEST(colorscheme_is_restored_on_sourcing_error)
 
 	make_abs_path(cfg.colors_dir, sizeof(cfg.colors_dir), TEST_DATA_PATH,
 			"scripts/", saved_cwd);
-	assert_success(exec_commands("colorscheme wrong-cmd-name", &lwin,
+	assert_success(cmds_dispatch("colorscheme wrong-cmd-name", &lwin,
 				CIT_COMMAND));
 
 	assert_string_equal("test-scheme", cfg.cs.name);
@@ -172,7 +172,7 @@ TEST(colorscheme_is_restored_on_multiple_loading_failures)
 	cfg.cs.color[WIN_COLOR].bg = 2;
 	cfg.cs.color[WIN_COLOR].attr = 3;
 
-	assert_success(exec_commands("colorscheme bad-cs-name bad-cmd bad-color",
+	assert_success(cmds_dispatch("colorscheme bad-cs-name bad-cmd bad-color",
 				&lwin, CIT_COMMAND));
 
 	assert_string_equal("test-scheme", cfg.cs.name);
@@ -188,7 +188,7 @@ TEST(first_usable_colorscheme_is_loaded)
 	cfg.cs.color[WIN_COLOR].bg = 2;
 	cfg.cs.color[WIN_COLOR].attr = 3;
 
-	assert_success(exec_commands("colorscheme bad-color good", &lwin,
+	assert_success(cmds_dispatch("colorscheme bad-color good", &lwin,
 				CIT_COMMAND));
 
 	assert_string_equal("good", cfg.cs.name);
diff --git a/tests/commands/comments.c b/tests/commands/comments.c
index f690544..98495bf 100644
--- a/tests/commands/comments.c
+++ b/tests/commands/comments.c
@@ -10,9 +10,9 @@
 
 TEST(whole_line_comments)
 {
-	assert_success(exec_command("\"", NULL, CIT_COMMAND));
-	assert_success(exec_command(" \"", NULL, CIT_COMMAND));
-	assert_success(exec_command("  \"", NULL, CIT_COMMAND));
+	assert_success(cmds_dispatch1("\"", NULL, CIT_COMMAND));
+	assert_success(cmds_dispatch1(" \"", NULL, CIT_COMMAND));
+	assert_success(cmds_dispatch1("  \"", NULL, CIT_COMMAND));
 }
 
 TEST(trailing_comments)
@@ -23,21 +23,21 @@ TEST(trailing_comments)
 	curr_view = &lwin;
 	other_view = &rwin;
 
-	init_commands();
+	cmds_init();
 
 	opt_handlers_setup();
 
-	assert_success(exec_command("let $a = 4 \"", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch1("let $a = 4 \"", &lwin, CIT_COMMAND));
 	assert_string_equal("4", env_get("a"));
-	assert_success(exec_command("let $a = \" 4 \"", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch1("let $a = \" 4 \"", &lwin, CIT_COMMAND));
 	assert_string_equal(" 4 ", env_get("a"));
-	assert_failure(exec_command("echo \" 4 \"", &lwin, CIT_COMMAND));
-	assert_success(exec_command("exe \" 4 \"", &lwin, CIT_COMMAND));
-	assert_success(exec_command("unlet $a \"comment", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch1("echo \" 4 \"", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch1("exe \" 4 \"", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch1("unlet $a \"comment", &lwin, CIT_COMMAND));
 	assert_string_equal(NULL, env_get("a"));
 
-	assert_success(exec_command("set statusline=\"  %t%= %A %15E %20d  \"", &lwin,
-				CIT_COMMAND));
+	assert_success(cmds_dispatch1("set statusline=\"  %t%= %A %15E %20d  \"",
+				&lwin, CIT_COMMAND));
 	assert_string_equal("  %t%= %A %15E %20d  ", cfg.status_line);
 
 	opt_handlers_teardown();
diff --git a/tests/commands/completion.c b/tests/commands/completion.c
index c7efc35..7e29cbe 100644
--- a/tests/commands/completion.c
+++ b/tests/commands/completion.c
@@ -77,7 +77,7 @@ SETUP()
 
 	curr_view = &lwin;
 
-	init_commands();
+	cmds_init();
 
 	vle_opts_init(&option_changed, NULL);
 	vle_opts_add("fusehome", "fh", "descr", OPT_STR, OPT_GLOBAL, 0, NULL,
@@ -256,7 +256,7 @@ TEST(bmark_tags_are_completed)
 {
 	bmarks_clear();
 
-	assert_success(exec_commands("bmark! fake/path1 tag1", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! fake/path1 tag1", &lwin, CIT_COMMAND));
 
 	ASSERT_COMPLETION(L"bmark tag", L"bmark tag1");
 	ASSERT_COMPLETION(L"bmark! fake/path2 tag", L"bmark! fake/path2 tag1");
@@ -282,7 +282,7 @@ TEST(delbmark_tags_are_completed)
 {
 	bmarks_clear();
 
-	assert_success(exec_commands("bmark! fake/path1 tag1", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! fake/path1 tag1", &lwin, CIT_COMMAND));
 
 	ASSERT_COMPLETION(L"delbmark ta", L"delbmark tag1");
 }
@@ -544,7 +544,7 @@ TEST(plugin_is_completed)
 
 	curr_stats.vlua = vlua_init();
 	curr_stats.plugs = plugs_create(curr_stats.vlua);
-	plugs_load(curr_stats.plugs, cfg.config_dir);
+	load_plugins(curr_stats.plugs, cfg.config_dir);
 
 	ASSERT_COMPLETION(L"plugin whitelist ", L"plugin whitelist plug1");
 	ASSERT_NEXT_MATCH("plug2");
@@ -588,7 +588,7 @@ TEST(highlight_is_completed)
 	ASSERT_COMPLETION(L"hi WildMenu guibg=r", L"hi WildMenu guibg=red");
 	ASSERT_COMPLETION(L"hi WildMenu guibg=l", L"hi WildMenu guibg=l");
 
-	assert_success(exec_commands("hi {*.jpg} cterm=none", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("hi {*.jpg} cterm=none", &lwin, CIT_COMMAND));
 	ASSERT_COMPLETION(L"hi clear ", L"hi clear {*.jpg}");
 }
 
diff --git a/tests/commands/cpmv.c b/tests/commands/cpmv.c
index d61a5d4..4a0e2e7 100644
--- a/tests/commands/cpmv.c
+++ b/tests/commands/cpmv.c
@@ -45,7 +45,7 @@ SETUP()
 	make_abs_path(rwin.curr_dir, sizeof(rwin.curr_dir), SANDBOX_PATH, "right",
 			cwd);
 
-	init_commands();
+	cmds_init();
 
 	create_dir(SANDBOX_PATH "/left");
 	make_file(SANDBOX_PATH "/left/a", "1");
@@ -75,18 +75,18 @@ TEARDOWN()
 TEST(wrong_cpmv_flag)
 {
 	ui_sb_msg("");
-	assert_failure(exec_commands("copy -wrong", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("copy -wrong", &lwin, CIT_COMMAND));
 	assert_string_equal("Unrecognized :command option: -wrong", ui_sb_last());
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("alink -wrong", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("alink -wrong", &lwin, CIT_COMMAND));
 	assert_string_equal("Unrecognized :command option: -wrong", ui_sb_last());
 }
 
 TEST(copy_can_skip_existing_files)
 {
 	ui_sb_msg("");
-	assert_failure(exec_commands("%copy -skip", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("%copy -skip", &lwin, CIT_COMMAND));
 	assert_string_equal("2 files successfully processed", ui_sb_last());
 
 	assert_int_equal(2, get_file_size(SANDBOX_PATH "/right/a"));
@@ -96,7 +96,7 @@ TEST(copy_can_skip_existing_files)
 TEST(link_can_skip_existing_files, IF(not_windows))
 {
 	ui_sb_msg("");
-	assert_failure(exec_commands("%alink -skip", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("%alink -skip", &lwin, CIT_COMMAND));
 	assert_string_equal("2 files successfully processed", ui_sb_last());
 
 	assert_int_equal(2, get_file_size(SANDBOX_PATH "/right/a"));
@@ -106,7 +106,7 @@ TEST(link_can_skip_existing_files, IF(not_windows))
 TEST(file_name_can_start_with_a_dash)
 {
 	ui_sb_msg("");
-	assert_failure(exec_commands("%copy -- -test -skip", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("%copy -- -test -skip", &lwin, CIT_COMMAND));
 	assert_string_equal("2 files successfully processed", ui_sb_last());
 
 	remove_file(SANDBOX_PATH "/right/-test");
@@ -127,7 +127,7 @@ TEST(cpmv_does_not_crash_on_wrong_list_access)
 	strcpy(lwin.curr_dir, path);
 	strcpy(rwin.curr_dir, sandbox);
 
-	free_dir_entries(&lwin, &lwin.dir_entry, &lwin.list_rows);
+	free_dir_entries(&lwin.dir_entry, &lwin.list_rows);
 
 	lwin.list_rows = 3;
 	lwin.list_pos = 0;
@@ -146,7 +146,7 @@ TEST(cpmv_does_not_crash_on_wrong_list_access)
 
 	/* cpmv used to use presence of the argument as indication of availability of
 	 * file list and access memory beyond array boundaries. */
-	(void)exec_commands("co .", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("co .", &lwin, CIT_COMMAND);
 
 	snprintf(path, sizeof(path), "%s/a", sandbox);
 	assert_success(remove(path));
diff --git a/tests/commands/edit.c b/tests/commands/edit.c
new file mode 100644
index 0000000..ed29d96
--- /dev/null
+++ b/tests/commands/edit.c
@@ -0,0 +1,148 @@
+#include <stic.h>
+
+#include <stdio.h> /* FILE fclose() fopen() fprintf() remove() */
+#include <string.h> /* strcpy() strdup() */
+
+#include <test-utils.h>
+
+#include "../../src/cfg/config.h"
+#include "../../src/compat/fs_limits.h"
+#include "../../src/compat/os.h"
+#include "../../src/lua/vlua.h"
+#include "../../src/ui/statusbar.h"
+#include "../../src/ui/ui.h"
+#include "../../src/utils/dynarray.h"
+#include "../../src/utils/fs.h"
+#include "../../src/utils/str.h"
+#include "../../src/cmd_core.h"
+#include "../../src/status.h"
+
+static char sandbox[PATH_MAX + 1];
+
+SETUP_ONCE()
+{
+	char cwd[PATH_MAX + 1];
+	assert_non_null(get_cwd(cwd, sizeof(cwd)));
+
+	make_abs_path(sandbox, sizeof(sandbox), SANDBOX_PATH, "", cwd);
+}
+
+SETUP()
+{
+	conf_setup();
+	view_setup(&lwin);
+	cmds_init();
+
+	curr_view = &lwin;
+}
+
+TEARDOWN()
+{
+	conf_teardown();
+	view_teardown(&lwin);
+	vle_cmds_reset();
+
+	curr_view = NULL;
+}
+
+TEST(edit_handles_ranges, IF(not_windows))
+{
+	create_file(SANDBOX_PATH "/file1");
+	create_file(SANDBOX_PATH "/file2");
+
+	char script_path[PATH_MAX + 1];
+	make_abs_path(script_path, sizeof(script_path), sandbox, "script", NULL);
+	update_string(&cfg.vi_command, script_path);
+	update_string(&cfg.vi_x_command, "");
+
+	FILE *fp = fopen(SANDBOX_PATH "/script", "w");
+	fprintf(fp, "#!/bin/sh\n");
+	fprintf(fp, "for arg; do echo \"$arg\" >> %s/vi-list; done\n", SANDBOX_PATH);
+	fclose(fp);
+	assert_success(os_chmod(SANDBOX_PATH "/script", 0777));
+
+	strcpy(lwin.curr_dir, sandbox);
+	lwin.list_rows = 2;
+	lwin.list_pos = 0;
+	lwin.dir_entry = dynarray_cextend(NULL,
+			lwin.list_rows*sizeof(*lwin.dir_entry));
+	lwin.dir_entry[0].name = strdup("file1");
+	lwin.dir_entry[0].origin = &lwin.curr_dir[0];
+	lwin.dir_entry[1].name = strdup("file2");
+	lwin.dir_entry[1].origin = &lwin.curr_dir[0];
+
+	(void)cmds_dispatch("%edit", &lwin, CIT_COMMAND);
+
+	const char *lines[] = { "file1", "file2" };
+	file_is(SANDBOX_PATH "/vi-list", lines, ARRAY_LEN(lines));
+
+	assert_success(remove(SANDBOX_PATH "/script"));
+	assert_success(remove(SANDBOX_PATH "/file1"));
+	assert_success(remove(SANDBOX_PATH "/file2"));
+	assert_success(remove(SANDBOX_PATH "/vi-list"));
+}
+
+TEST(edit_command)
+{
+	curr_stats.exec_env_type = EET_EMULATOR;
+	update_string(&cfg.vi_command, "#vifmtest#editor");
+	cfg.config_dir[0] = '\0';
+
+	curr_stats.vlua = vlua_init();
+
+	assert_success(vlua_run_string(curr_stats.vlua,
+				"function handler(info)"
+				"  local s = ginfo ~= nil"
+				"  ginfo = info"
+				"  return { success = s }"
+				"end"));
+	assert_success(vlua_run_string(curr_stats.vlua,
+				"vifm.addhandler{ name = 'editor', handler = handler }"));
+
+	int i;
+	for(i = 0; i < 2; ++i)
+	{
+		assert_success(cmds_dispatch("edit a b", &lwin, CIT_COMMAND));
+
+		assert_success(vlua_run_string(curr_stats.vlua, "print(ginfo.action)"));
+		assert_string_equal("edit-many", ui_sb_last());
+		assert_success(vlua_run_string(curr_stats.vlua, "print(#ginfo.paths)"));
+		assert_string_equal("2", ui_sb_last());
+		assert_success(vlua_run_string(curr_stats.vlua, "print(ginfo.paths[1])"));
+		assert_string_equal("a", ui_sb_last());
+		assert_success(vlua_run_string(curr_stats.vlua, "print(ginfo.paths[2])"));
+		assert_string_equal("b", ui_sb_last());
+
+		assert_success(vlua_run_string(curr_stats.vlua, "ginfo = {}"));
+	}
+
+	vlua_finish(curr_stats.vlua);
+	curr_stats.vlua = NULL;
+}
+
+TEST(edit_broken_symlink, IF(not_windows))
+{
+	make_symlink("no-such-file", SANDBOX_PATH "/broken");
+
+	update_string(&cfg.vi_command, "rm");
+	update_string(&cfg.vi_x_command, "");
+
+	char *saved_cwd = save_cwd();
+	assert_success(chdir(sandbox));
+
+	strcpy(lwin.curr_dir, sandbox);
+	lwin.list_rows = 1;
+	lwin.list_pos = 0;
+	lwin.dir_entry = dynarray_cextend(NULL,
+			lwin.list_rows*sizeof(*lwin.dir_entry));
+	lwin.dir_entry[0].name = strdup("broken");
+	lwin.dir_entry[0].origin = &lwin.curr_dir[0];
+
+	(void)cmds_dispatch("edit", &lwin, CIT_COMMAND);
+
+	restore_cwd(saved_cwd);
+	remove_file(SANDBOX_PATH "/broken");
+}
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 : */
diff --git a/tests/commands/else.c b/tests/commands/else.c
index 4066fbb..be304cd 100644
--- a/tests/commands/else.c
+++ b/tests/commands/else.c
@@ -23,7 +23,7 @@ remove_tmp_vars(void)
 
 SETUP()
 {
-	init_commands();
+	cmds_init();
 	lwin.selected_files = 0;
 	remove_tmp_vars();
 	curr_view = &lwin;
@@ -40,7 +40,7 @@ TEST(if_without_else_true_condition)
 	                             " |     let $"VAR_A" = '"VAR_A"'"
 	                             " | endif";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal(VAR_A, env_get(VAR_A));
 }
 
@@ -50,7 +50,7 @@ TEST(if_without_else_false_condition)
 	                             " |     let $"VAR_A" = '"VAR_A"'"
 	                             " | endif";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal(NULL, env_get(VAR_A));
 }
 
@@ -61,7 +61,7 @@ TEST(if_with_else_true_condition)
 	                             " |     let $"VAR_A" = '"VAR_A"'"
 	                             " | endif";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal(NULL, env_get(VAR_A));
 }
 
@@ -72,7 +72,7 @@ TEST(if_with_else_false_condition)
 	                             " |     let $"VAR_A" = '"VAR_A"'"
 	                             " | endif";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal(VAR_A, env_get(VAR_A));
 }
 
@@ -86,7 +86,7 @@ TEST(if_true_if_true_condition)
 	                             " |     let $"VAR_C" = '"VAR_C"'"
 	                             " | endif";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal(VAR_A, env_get(VAR_A));
 	assert_string_equal(VAR_B, env_get(VAR_B));
 	assert_string_equal(VAR_C, env_get(VAR_C));
@@ -102,7 +102,7 @@ TEST(if_true_if_false_condition)
 	                             " |     let $"VAR_C" = '"VAR_C"'"
 	                             " | endif";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal(VAR_A, env_get(VAR_A));
 	assert_string_equal(NULL, env_get(VAR_B));
 	assert_string_equal(VAR_C, env_get(VAR_C));
@@ -118,7 +118,7 @@ TEST(if_false_if_true_condition)
 	                             " |     let $"VAR_C" = '"VAR_C"'"
 	                             " | endif";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal(NULL, env_get(VAR_A));
 	assert_string_equal(NULL, env_get(VAR_B));
 	assert_string_equal(NULL, env_get(VAR_C));
@@ -136,7 +136,7 @@ TEST(if_false_else_if_true_condition)
 	                             " |     let $"VAR_D" = '"VAR_D"'"
 	                             " | endif";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal(NULL, env_get(VAR_A));
 	assert_string_equal(VAR_B, env_get(VAR_B));
 	assert_string_equal(VAR_C, env_get(VAR_C));
@@ -155,7 +155,7 @@ TEST(if_false_if_else_condition)
 	                             " |     let $"VAR_D" = '"VAR_D"'"
 	                             " | endif";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal(NULL, env_get(VAR_A));
 	assert_string_equal(NULL, env_get(VAR_B));
 	assert_string_equal(NULL, env_get(VAR_C));
@@ -175,7 +175,7 @@ TEST(if_true_else_if_else_condition)
 	                             " |     let $"VAR_D" = '"VAR_D"'"
 	                             " | endif";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal(NULL, env_get(VAR_A));
 	assert_string_equal(NULL, env_get(VAR_B));
 	assert_string_equal(NULL, env_get(VAR_C));
@@ -191,7 +191,7 @@ TEST(if_true_elseif_else_condition)
 	                             " |     let $"VAR_C" = '"VAR_C"'"
 	                             " | endif";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal(NULL, env_get(VAR_A));
 	assert_string_equal(NULL, env_get(VAR_B));
 	assert_string_equal(NULL, env_get(VAR_C));
@@ -207,7 +207,7 @@ TEST(if_false_elseif_true_else_condition)
 	                             " |     let $"VAR_C" = '"VAR_C"'"
 	                             " | endif";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal(NULL, env_get(VAR_A));
 	assert_string_equal(VAR_B, env_get(VAR_B));
 	assert_string_equal(NULL, env_get(VAR_C));
@@ -223,7 +223,7 @@ TEST(if_false_elseif_false_else_condition)
 	                             " |     let $"VAR_C" = '"VAR_C"'"
 	                             " | endif";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal(NULL, env_get(VAR_A));
 	assert_string_equal(NULL, env_get(VAR_B));
 	assert_string_equal(VAR_C, env_get(VAR_C));
@@ -241,7 +241,7 @@ TEST(nested_passive_elseif)
 	                             " |     let $"VAR_C" = '"VAR_C"'"
 	                             " | endif";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal(NULL, env_get(VAR_A));
 	assert_string_equal(NULL, env_get(VAR_B));
 	assert_string_equal(VAR_C, env_get(VAR_C));
@@ -259,7 +259,7 @@ TEST(nested_active_elseif)
 	                             " |     let $"VAR_C" = '"VAR_C"'"
 	                             " | endif";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal(NULL, env_get(VAR_A));
 	assert_string_equal(VAR_B, env_get(VAR_B));
 	assert_string_equal(NULL, env_get(VAR_C));
@@ -274,7 +274,7 @@ TEST(else_before_elseif)
 	                             " | elseif 2 == 2"
 	                             " | endif";
 
-	assert_failure(exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 }
 
 TEST(multiple_else_branches)
@@ -284,7 +284,7 @@ TEST(multiple_else_branches)
 	                             " | else"
 	                             " | endif";
 
-	assert_failure(exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 }
 
 TEST(sourcing_in_body)
@@ -293,22 +293,22 @@ TEST(sourcing_in_body)
 	                         " |     source "TEST_DATA_PATH"/scripts/set-env.vifm"
 	                         " | endif";
 
-	assert_int_equal(0, exec_commands(CMDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(CMDS, &lwin, CIT_COMMAND));
 }
 
 TEST(wrong_expr_causes_hard_error)
 {
-	assert_true(exec_commands("if $USER == root", &lwin, CIT_COMMAND) < 0);
+	assert_true(cmds_dispatch("if $USER == root", &lwin, CIT_COMMAND) < 0);
 }
 
 TEST(misplaced_else_causes_hard_error)
 {
-	assert_true(exec_commands("else", &lwin, CIT_COMMAND) < 0);
+	assert_true(cmds_dispatch("else", &lwin, CIT_COMMAND) < 0);
 }
 
 TEST(misplaced_endif_causes_hard_error)
 {
-	assert_true(exec_commands("endif", &lwin, CIT_COMMAND) < 0);
+	assert_true(cmds_dispatch("endif", &lwin, CIT_COMMAND) < 0);
 }
 
 TEST(finish_inside_if_statement_causes_no_missing_endif_error)
@@ -319,7 +319,7 @@ TEST(finish_inside_if_statement_causes_no_missing_endif_error)
 		" |     source "TEST_DATA_PATH"/scripts/finish-inside-ifs.vifm"
 		" | endif";
 
-	assert_int_equal(0, exec_commands(CMDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(CMDS, &lwin, CIT_COMMAND));
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/tests/commands/emark.c b/tests/commands/emark.c
index 917b8d0..f931395 100644
--- a/tests/commands/emark.c
+++ b/tests/commands/emark.c
@@ -45,7 +45,7 @@ SETUP()
 	curr_view = &lwin;
 	other_view = &rwin;
 
-	init_commands();
+	cmds_init();
 
 	vle_cmds_add(commands, ARRAY_LEN(commands));
 
@@ -69,26 +69,26 @@ builtin_cmd(const cmd_info_t* cmd_info)
 TEST(repeat_of_no_command_prints_a_message)
 {
 	called = 0;
-	(void)exec_commands("builtin", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("builtin", &lwin, CIT_COMMAND);
 	assert_int_equal(1, called);
 
 	update_string(&curr_stats.last_cmdline_command, NULL);
 
 	called = 0;
-	assert_int_equal(1, exec_commands("!!", &lwin, CIT_COMMAND));
+	assert_int_equal(1, cmds_dispatch("!!", &lwin, CIT_COMMAND));
 	assert_int_equal(0, called);
 }
 
 TEST(double_emark_repeats_last_command)
 {
 	called = 0;
-	(void)exec_commands("builtin", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("builtin", &lwin, CIT_COMMAND);
 	assert_int_equal(1, called);
 
 	update_string(&curr_stats.last_cmdline_command, "builtin");
 
 	called = 0;
-	assert_int_equal(0, exec_commands("!!", &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch("!!", &lwin, CIT_COMMAND));
 	assert_int_equal(1, called);
 }
 
@@ -97,7 +97,7 @@ TEST(single_emark_without_args_fails)
 	update_string(&curr_stats.last_cmdline_command, "builtin");
 
 	called = 0;
-	assert_false(exec_commands("!", &lwin, CIT_COMMAND) == 0);
+	assert_false(cmds_dispatch("!", &lwin, CIT_COMMAND) == 0);
 	assert_int_equal(0, called);
 }
 
@@ -114,7 +114,7 @@ TEST(provide_input_to_fg_process, IF(have_cat))
 	lwin.dir_entry[1].marked = 1;
 	lwin.pending_marking = 1;
 
-	assert_int_equal(0, exec_commands("!cat > file %Pl", &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch("!cat > file %Pl", &lwin, CIT_COMMAND));
 
 	const char *lines[] = { "/path/a", "/path/b" };
 	file_is("file", lines, ARRAY_LEN(lines));
@@ -137,7 +137,7 @@ TEST(provide_input_to_bg_process, IF(have_cat))
 	lwin.dir_entry[1].marked = 1;
 	lwin.pending_marking = 1;
 
-	assert_int_equal(0, exec_commands("!cat > file %Pl &", &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch("!cat > file %Pl &", &lwin, CIT_COMMAND));
 
 	wait_for_all_bg();
 
diff --git a/tests/commands/filetype.c b/tests/commands/filetype.c
index bc3c252..91697c0 100644
--- a/tests/commands/filetype.c
+++ b/tests/commands/filetype.c
@@ -17,7 +17,7 @@ static int has_mime_type_detection(void);
 
 SETUP()
 {
-	init_commands();
+	cmds_init();
 	conf_setup();
 
 	view_setup(&lwin);
@@ -40,7 +40,7 @@ TEST(filetype_accepts_negated_patterns)
 {
 	ft_init(&prog_exists);
 
-	assert_success(exec_commands("filetype !{*.tar} prog", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filetype !{*.tar} prog", &lwin, CIT_COMMAND));
 
 	check_filetype();
 
@@ -52,7 +52,7 @@ TEST(filextype_accepts_negated_patterns)
 	ft_init(&prog_exists);
 	curr_stats.exec_env_type = EET_EMULATOR_WITH_X;
 
-	assert_success(exec_commands("filextype !{*.tar} prog", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filextype !{*.tar} prog", &lwin, CIT_COMMAND));
 
 	check_filetype();
 
@@ -66,7 +66,7 @@ TEST(fileviewer_accepts_negated_patterns)
 {
 	ft_init(&prog_exists);
 
-	assert_success(exec_commands("fileviewer !{*.tar} view", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("fileviewer !{*.tar} view", &lwin, CIT_COMMAND));
 	assert_string_equal("view", ft_get_viewer("file.version.tar.bz2"));
 
 	ft_reset(0);
@@ -75,9 +75,9 @@ TEST(fileviewer_accepts_negated_patterns)
 TEST(pattern_anding_and_orring_failures)
 {
 	/* No matching is performed, so we can use application/octet-stream. */
-	assert_failure(exec_commands("filetype /*/,"
+	assert_failure(cmds_dispatch("filetype /*/,"
 				"<application/octet-stream>{binary-data} app", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("fileviewer /*/,"
+	assert_failure(cmds_dispatch("fileviewer /*/,"
 				"<application/octet-stream>{binary-data} viewer", &lwin, CIT_COMMAND));
 }
 
@@ -91,11 +91,11 @@ TEST(pattern_anding_and_orring, IF(has_mime_type_detection))
 	snprintf(cmd, sizeof(cmd),
 			"filetype {two-lines}<text/plain>,<%s>{binary-data} app",
 			get_mimetype(TEST_DATA_PATH "/read/binary-data", 0));
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 	snprintf(cmd, sizeof(cmd),
 			"fileviewer {two-lines}<text/plain>,<%s>{binary-data} viewer",
 			get_mimetype(TEST_DATA_PATH "/read/binary-data", 0));
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 
 	ft = ft_get_all_programs(TEST_DATA_PATH "/read/two-lines");
 	assert_int_equal(1, ft.count);
@@ -128,7 +128,7 @@ TEST(pattern_anding_and_orring, IF(has_mime_type_detection))
 
 TEST(cv_is_built_by_handler)
 {
-	init_modes();
+	modes_init();
 
 	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), TEST_DATA_PATH, "", NULL);
 
@@ -136,8 +136,8 @@ TEST(cv_is_built_by_handler)
 	assert_non_null(flist_custom_add(&lwin, "existing-files/a"));
 	assert_success(flist_custom_finish(&lwin, CV_REGULAR, 0));
 
-	assert_success(exec_commands("filetype a echo %c %u", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("normal l", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filetype a echo %c %u", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("normal l", &lwin, CIT_COMMAND));
 	assert_true(flist_custom_active(&lwin));
 
 	assert_string_equal("echo %c %u", lwin.custom.title);
diff --git a/tests/commands/filter.c b/tests/commands/filter.c
index 538cc24..f390c70 100644
--- a/tests/commands/filter.c
+++ b/tests/commands/filter.c
@@ -23,7 +23,7 @@ SETUP()
 	curr_view = &lwin;
 	other_view = &rwin;
 
-	init_commands();
+	cmds_init();
 }
 
 TEARDOWN()
@@ -42,7 +42,7 @@ TEST(filter_prints_empty_filters_correctly)
 	                       "Implicit             ";
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("filter?", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("filter?", &lwin, CIT_COMMAND));
 	assert_string_equal(expected, ui_sb_last());
 }
 
@@ -53,11 +53,11 @@ TEST(filter_prints_non_empty_filters)
 	                       "Explicit    ---->    abc\n"
 	                       "Implicit             ";
 
-	assert_success(exec_commands("filter abc", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filter abc", &lwin, CIT_COMMAND));
 	local_filter_apply(&lwin, "local");
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("filter?", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("filter?", &lwin, CIT_COMMAND));
 	assert_string_equal(expected, ui_sb_last());
 }
 
@@ -71,16 +71,16 @@ TEST(filter_with_empty_value_reuses_last_search)
 	cfg_resize_histories(5);
 	hists_search_save("pattern");
 
-	assert_success(exec_commands("filter //I", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filter //I", &lwin, CIT_COMMAND));
 	ui_sb_msg("");
-	assert_failure(exec_commands("filter?", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("filter?", &lwin, CIT_COMMAND));
 	assert_string_equal(expected, ui_sb_last());
 }
 
 TEST(filter_accepts_pipe_without_escaping)
 {
-	assert_success(exec_commands("filter /a|b/", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("filter a|b", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filter /a|b/", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filter a|b", &lwin, CIT_COMMAND));
 }
 
 TEST(filter_prints_whole_manual_filter_expression)
@@ -90,10 +90,10 @@ TEST(filter_prints_whole_manual_filter_expression)
 	                       "Explicit    ---->    /abc/i\n"
 	                       "Implicit             ";
 
-	assert_success(exec_commands("filter /abc/i", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filter /abc/i", &lwin, CIT_COMMAND));
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("filter?", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("filter?", &lwin, CIT_COMMAND));
 	assert_string_equal(expected, ui_sb_last());
 }
 
@@ -104,11 +104,11 @@ TEST(filter_without_args_resets_manual_filter)
 	                       "Explicit             \n"
 	                       "Implicit             ";
 
-	assert_success(exec_commands("filter this", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("filter", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filter this", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filter", &lwin, CIT_COMMAND));
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("filter?", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("filter?", &lwin, CIT_COMMAND));
 	assert_string_equal(expected, ui_sb_last());
 }
 
@@ -122,11 +122,11 @@ TEST(filter_reset_is_not_affected_by_search_history)
 	cfg_resize_histories(5);
 	hists_search_save("pattern");
 
-	assert_success(exec_commands("filter this", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("filter", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filter this", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filter", &lwin, CIT_COMMAND));
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("filter?", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("filter?", &lwin, CIT_COMMAND));
 	assert_string_equal(expected, ui_sb_last());
 }
 
@@ -138,7 +138,7 @@ TEST(filter_can_affect_both_views)
 	other_view->invert = 1;
 
 	curr_stats.global_local_settings = 1;
-	assert_success(exec_commands("filter /x/", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filter /x/", &lwin, CIT_COMMAND));
 	curr_stats.global_local_settings = 0;
 
 	assert_string_equal("/x/", matcher_get_expr(curr_view->manual_filter));
@@ -152,7 +152,7 @@ TEST(filter_can_setup_inverted_filter)
 	assert_string_equal("", matcher_get_expr(curr_view->manual_filter));
 	curr_view->invert = 0;
 
-	assert_success(exec_commands("filter! /x/", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filter! /x/", &lwin, CIT_COMMAND));
 
 	assert_string_equal("/x/", matcher_get_expr(curr_view->manual_filter));
 	assert_true(curr_view->invert);
@@ -161,20 +161,20 @@ TEST(filter_can_setup_inverted_filter)
 TEST(filter_can_invert_manual_filter)
 {
 	curr_view->invert = 0;
-	assert_success(exec_commands("filter!", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filter!", &lwin, CIT_COMMAND));
 	assert_true(curr_view->invert);
-	assert_success(exec_commands("filter!", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filter!", &lwin, CIT_COMMAND));
 	assert_false(curr_view->invert);
 }
 
 TEST(filter_accepts_full_path_patterns)
 {
-	assert_success(exec_commands("filter ///some/path//", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filter ///some/path//", &lwin, CIT_COMMAND));
 }
 
 TEST(filter_accepts_paths_with_many_spaces)
 {
-	assert_success(exec_commands("filter { a b c d e }", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filter { a b c d e }", &lwin, CIT_COMMAND));
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/tests/commands/generic.c b/tests/commands/generic.c
index a8ceb71..afc35ea 100644
--- a/tests/commands/generic.c
+++ b/tests/commands/generic.c
@@ -13,6 +13,7 @@
 #include "../../src/cfg/config.h"
 #include "../../src/engine/cmds.h"
 #include "../../src/engine/keys.h"
+#include "../../src/engine/mode.h"
 #include "../../src/modes/modes.h"
 #include "../../src/ui/ui.h"
 #include "../../src/utils/dynarray.h"
@@ -65,7 +66,7 @@ SETUP()
 	conf_setup();
 	cfg.use_system_calls = 1;
 
-	init_commands();
+	cmds_init();
 
 	vle_cmds_add(commands, ARRAY_LEN(commands));
 
@@ -102,35 +103,35 @@ builtin_cmd(const cmd_info_t* cmd_info)
 
 TEST(space_amp)
 {
-	assert_success(exec_commands("builtin &", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("builtin &", &lwin, CIT_COMMAND));
 	assert_true(called);
 	assert_true(bg);
 }
 
 TEST(space_amp_spaces)
 {
-	assert_success(exec_commands("builtin &    ", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("builtin &    ", &lwin, CIT_COMMAND));
 	assert_true(called);
 	assert_true(bg);
 }
 
 TEST(space_bg_bar)
 {
-	assert_success(exec_commands("builtin &|", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("builtin &|", &lwin, CIT_COMMAND));
 	assert_true(called);
 	assert_true(bg);
 }
 
 TEST(bg_space_bar)
 {
-	assert_success(exec_commands("builtin& |", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("builtin& |", &lwin, CIT_COMMAND));
 	assert_true(called);
 	assert_true(bg);
 }
 
 TEST(space_bg_space_bar)
 {
-	assert_success(exec_commands("builtin & |", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("builtin & |", &lwin, CIT_COMMAND));
 	assert_true(called);
 	assert_true(bg);
 }
@@ -138,7 +139,7 @@ TEST(space_bg_space_bar)
 TEST(non_printable_arg)
 {
 	/* \x0C is Ctrl-L. */
-	assert_success(exec_commands("onearg \x0C", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("onearg \x0C", &lwin, CIT_COMMAND));
 	assert_true(called);
 	assert_string_equal("\x0C", arg);
 }
@@ -146,36 +147,36 @@ TEST(non_printable_arg)
 TEST(non_printable_arg_in_udf)
 {
 	/* \x0C is Ctrl-L. */
-	assert_success(exec_commands("command udf :onearg \x0C", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("command udf :onearg \x0C", &lwin, CIT_COMMAND));
 
-	assert_success(exec_commands("udf", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("udf", &lwin, CIT_COMMAND));
 	assert_true(called);
 	assert_string_equal("\x0C", arg);
 }
 
 TEST(space_last_arg_in_udf)
 {
-	assert_success(exec_commands("command udf :onearg \\ ", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("command udf :onearg \\ ", &lwin, CIT_COMMAND));
 
-	assert_success(exec_commands("udf", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("udf", &lwin, CIT_COMMAND));
 	assert_true(called);
 	assert_string_equal(" ", arg);
 }
 
 TEST(bg_mark_with_space_in_udf)
 {
-	assert_success(exec_commands("command udf :builtin &", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("command udf :builtin &", &lwin, CIT_COMMAND));
 
-	assert_success(exec_commands("udf", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("udf", &lwin, CIT_COMMAND));
 	assert_true(called);
 	assert_true(bg);
 }
 
 TEST(bg_mark_without_space_in_udf)
 {
-	assert_success(exec_commands("command udf :builtin&", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("command udf :builtin&", &lwin, CIT_COMMAND));
 
-	assert_success(exec_commands("udf", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("udf", &lwin, CIT_COMMAND));
 	assert_true(called);
 	assert_true(bg);
 }
@@ -193,12 +194,12 @@ TEST(shell_invocation_works_in_udf)
 
 	assert_success(chdir(SANDBOX_PATH));
 
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 
 	curr_view = &lwin;
 
 	assert_failure(access("out", F_OK));
-	assert_success(exec_commands("udf", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("udf", &lwin, CIT_COMMAND));
 	assert_success(access("out", F_OK));
 	assert_success(unlink("out"));
 }
@@ -210,48 +211,48 @@ TEST(envvars_of_commands_come_from_variables_unit)
 	strcpy(lwin.curr_dir, test_data);
 
 	assert_false(is_root_dir(lwin.curr_dir));
-	assert_success(exec_commands("let $ABCDE = '/'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("let $ABCDE = '/'", &lwin, CIT_COMMAND));
 	env_set("ABCDE", SANDBOX_PATH);
-	assert_success(exec_commands("cd $ABCDE", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cd $ABCDE", &lwin, CIT_COMMAND));
 	assert_true(is_root_dir(lwin.curr_dir));
 }
 
 TEST(or_operator_is_attributed_to_echo)
 {
-	(void)exec_commands("echo 1 || builtin", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("echo 1 || builtin", &lwin, CIT_COMMAND);
 	assert_false(called);
 }
 
 TEST(bar_is_not_attributed_to_echo)
 {
-	(void)exec_commands("echo 1 | builtin", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("echo 1 | builtin", &lwin, CIT_COMMAND);
 	assert_true(called);
 }
 
 TEST(mixed_or_operator_and_bar)
 {
-	(void)exec_commands("echo 1 || 0 | builtin", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("echo 1 || 0 | builtin", &lwin, CIT_COMMAND);
 	assert_true(called);
 }
 
 TEST(or_operator_is_attributed_to_if)
 {
-	(void)exec_commands("if 0 || 0 | builtin | endif", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("if 0 || 0 | builtin | endif", &lwin, CIT_COMMAND);
 	assert_false(called);
 }
 
 TEST(or_operator_is_attributed_to_let)
 {
-	(void)exec_commands("let $a = 'x'", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("let $a = 'x'", &lwin, CIT_COMMAND);
 	assert_string_equal("x", env_get("a"));
-	(void)exec_commands("let $a = 0 || 1", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("let $a = 0 || 1", &lwin, CIT_COMMAND);
 	assert_string_equal("1", env_get("a"));
 }
 
 TEST(user_command_is_executed_in_separated_scope)
 {
-	assert_success(exec_commands("command cmd :if 1 > 2", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("cmd", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("command cmd :if 1 > 2", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("cmd", &lwin, CIT_COMMAND));
 }
 
 TEST(usercmd_can_provide_input_to_fg_process, IF(have_cat))
@@ -262,14 +263,14 @@ TEST(usercmd_can_provide_input_to_fg_process, IF(have_cat))
 	replace_string(&lwin.dir_entry[0].name, "a");
 	replace_string(&lwin.dir_entry[1].name, "b");
 
-	assert_int_equal(0, exec_commands("command list cat > file %Pl", &lwin,
+	assert_int_equal(0, cmds_dispatch("command list cat > file %Pl", &lwin,
 				CIT_COMMAND));
 
 	lwin.dir_entry[0].marked = 1;
 	lwin.dir_entry[1].marked = 1;
 	lwin.pending_marking = 1;
 
-	assert_int_equal(0, exec_commands("list", &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch("list", &lwin, CIT_COMMAND));
 
 	const char *lines[] = { "/path/a", "/path/b" };
 	file_is("file", lines, ARRAY_LEN(lines));
@@ -285,14 +286,14 @@ TEST(usercmd_can_provide_input_to_bg_process, IF(have_cat))
 	replace_string(&lwin.dir_entry[0].name, "a");
 	replace_string(&lwin.dir_entry[1].name, "b");
 
-	assert_int_equal(0, exec_commands("command list cat > file %Pl &", &lwin,
+	assert_int_equal(0, cmds_dispatch("command list cat > file %Pl &", &lwin,
 				CIT_COMMAND));
 
 	lwin.dir_entry[0].marked = 1;
 	lwin.dir_entry[1].marked = 1;
 	lwin.pending_marking = 1;
 
-	assert_int_equal(0, exec_commands("list", &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch("list", &lwin, CIT_COMMAND));
 
 	wait_for_all_bg();
 
@@ -310,7 +311,7 @@ TEST(cv_is_built_by_emark)
 	assert_non_null(flist_custom_add(&lwin, "existing-files/a"));
 	assert_success(flist_custom_finish(&lwin, CV_REGULAR, 0));
 
-	assert_success(exec_commands("!echo %c %u", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("!echo %c %u", &lwin, CIT_COMMAND));
 	assert_true(flist_custom_active(&lwin));
 
 	assert_string_equal("!echo %c %u", lwin.custom.title);
@@ -332,7 +333,7 @@ TEST(title_of_cv_is_limited, IF(not_windows))
 	assert_non_null(flist_custom_add(&lwin, "existing-files/a"));
 	assert_success(flist_custom_finish(&lwin, CV_REGULAR, 0));
 
-	assert_success(exec_commands(long_cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(long_cmd, &lwin, CIT_COMMAND));
 	assert_true(flist_custom_active(&lwin));
 
 	assert_string_equal(title, lwin.custom.title);
@@ -346,8 +347,8 @@ TEST(cv_is_built_by_usercmd)
 	assert_non_null(flist_custom_add(&lwin, "existing-files/a"));
 	assert_success(flist_custom_finish(&lwin, CV_REGULAR, 0));
 
-	assert_success(exec_commands("command cmd echo %c %u", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("cmd", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("command cmd echo %c %u", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cmd", &lwin, CIT_COMMAND));
 	assert_true(flist_custom_active(&lwin));
 
 	assert_string_equal(":cmd", lwin.custom.title);
@@ -361,8 +362,8 @@ TEST(tree_cv_keeps_title)
 	assert_non_null(flist_custom_add(&lwin, "existing-files/a"));
 	assert_success(flist_custom_finish(&lwin, CV_REGULAR, 0));
 
-	assert_success(exec_commands("!echo %c %u", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("tree", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("!echo %c %u", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tree", &lwin, CIT_COMMAND));
 	assert_true(flist_custom_active(&lwin));
 
 	assert_string_equal("!echo %c %u", lwin.custom.title);
@@ -373,43 +374,44 @@ TEST(put_bg_cmd_is_parsed_correctly)
 	/* Simulate custom view to force failure of the command. */
 	lwin.curr_dir[0] = '\0';
 
-	assert_success(exec_commands("put \" &", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("put \" &", &lwin, CIT_COMMAND));
 }
 
 TEST(conversion_failure_is_handled)
 {
 	assert_non_null(setlocale(LC_ALL, "C"));
-	init_modes();
+	modes_init();
 
 	/* Execution of the following commands just shouldn't crash. */
-	(void)exec_commands("nnoremap \xee\x85\x8b", &lwin, CIT_COMMAND);
-	(void)exec_commands("nnoremap \xee\x85\x8b tj", &lwin, CIT_COMMAND);
-	(void)exec_commands("nnoremap tj \xee\x85\x8b", &lwin, CIT_COMMAND);
-	(void)exec_commands("nunmap \xee\x85\x8b", &lwin, CIT_COMMAND);
-	(void)exec_commands("unmap \xee\x85\x8b", &lwin, CIT_COMMAND);
-	(void)exec_commands("cabbrev \xee\x85\x8b tj", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("nnoremap \xee\x85\x8b", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("nnoremap \xee\x85\x8b tj", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("nnoremap tj \xee\x85\x8b", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("nunmap \xee\x85\x8b", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("unmap \xee\x85\x8b", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("cabbrev \xee\x85\x8b tj", &lwin, CIT_COMMAND);
 	/* The next command is needed so that there will be something to list. */
-	(void)exec_commands("cabbrev a b", &lwin, CIT_COMMAND);
-	(void)exec_commands("cabbrev \xee\x85\x8b", &lwin, CIT_COMMAND);
-	(void)exec_commands("cunabbrev \xee\x85\x8b", &lwin, CIT_COMMAND);
-	(void)exec_commands("normal \xee\x85\x8b", &lwin, CIT_COMMAND);
-	(void)exec_commands("wincmd \xee", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("cabbrev a b", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("cabbrev \xee\x85\x8b", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("cunabbrev \xee\x85\x8b", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("normal \xee\x85\x8b", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("wincmd \xee", &lwin, CIT_COMMAND);
 
 	vle_keys_reset();
 }
 
 TEST(selection_is_not_reset_in_visual_mode)
 {
-	init_modes();
+	modes_init();
 
 	init_view_list(&lwin);
 	update_string(&lwin.dir_entry[0].name, "name");
 
-	(void)exec_commands("if 1 == 1 | execute 'norm! ggvG' | endif", &lwin,
+	(void)cmds_dispatch("if 1 == 1 | execute 'norm! ggvG' | endif", &lwin,
 			CIT_COMMAND);
 
 	assert_true(lwin.dir_entry[0].selected);
 	assert_int_equal(1, lwin.selected_files);
+	assert_true(vle_mode_is(VISUAL_MODE));
 
 	vle_keys_reset();
 }
@@ -417,7 +419,7 @@ TEST(selection_is_not_reset_in_visual_mode)
 TEST(usercmd_range_is_as_good_as_selection)
 {
 	stats_init(&cfg);
-	init_modes();
+	modes_init();
 	regs_init();
 
 	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), test_data, "", cwd);
@@ -425,8 +427,8 @@ TEST(usercmd_range_is_as_good_as_selection)
 
 	/* For gA. */
 
-	assert_success(exec_commands("command! size :normal gA", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("%size", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("command! size :normal gA", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("%size", &lwin, CIT_COMMAND));
 	wait_for_bg();
 
 	assert_string_equal("color-schemes", lwin.dir_entry[0].name);
@@ -437,9 +439,9 @@ TEST(usercmd_range_is_as_good_as_selection)
 
 	/* For zf. */
 
-	assert_success(exec_commands("command! afilter :normal zf", &lwin,
+	assert_success(cmds_dispatch("command! afilter :normal zf", &lwin,
 				CIT_COMMAND));
-	assert_success(exec_commands("%afilter", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("%afilter", &lwin, CIT_COMMAND));
 	populate_dir_list(&lwin, 1);
 	assert_int_equal(1, lwin.list_rows);
 	assert_string_equal("..", lwin.dir_entry[0].name);
@@ -451,9 +453,9 @@ TEST(usercmd_range_is_as_good_as_selection)
 	assert_non_null(flist_custom_add(&lwin, "existing-files/b"));
 	assert_success(flist_custom_finish(&lwin, CV_REGULAR, 0));
 
-	assert_success(exec_commands("command! exclude :normal zd", &lwin,
+	assert_success(cmds_dispatch("command! exclude :normal zd", &lwin,
 				CIT_COMMAND));
-	assert_success(exec_commands("%exclude", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("%exclude", &lwin, CIT_COMMAND));
 	assert_int_equal(1, lwin.list_rows);
 	assert_string_equal("..", lwin.dir_entry[0].name);
 
@@ -464,14 +466,14 @@ TEST(usercmd_range_is_as_good_as_selection)
 	assert_non_null(flist_custom_add(&lwin, "existing-files/b"));
 	assert_success(flist_custom_finish(&lwin, CV_REGULAR, 0));
 
-	reg_t *reg = regs_find('"');
+	const reg_t *reg = regs_find('"');
 
-	assert_success(exec_commands("command! myyank :yank", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("%myyank", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("command! myyank :yank", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("%myyank", &lwin, CIT_COMMAND));
 	assert_int_equal(2, reg->nfiles);
 
-	assert_success(exec_commands("command! myyank :yank %a", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("%myyank", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("command! myyank :yank %a", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("%myyank", &lwin, CIT_COMMAND));
 	assert_int_equal(2, reg->nfiles);
 
 #ifndef _WIN32
@@ -497,8 +499,8 @@ TEST(usercmd_range_is_as_good_as_selection)
 	assert_non_null(flist_custom_add(&lwin, "existing-files/b"));
 	assert_success(flist_custom_finish(&lwin, CV_REGULAR, 0));
 
-	assert_success(exec_commands("command! run :normal l", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("%run", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("command! run :normal l", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("%run", &lwin, CIT_COMMAND));
 
 	const char *lines[] = { "existing-files/a", "existing-files/b" };
 	file_is(SANDBOX_PATH "/vi-list", lines, ARRAY_LEN(lines));
@@ -516,9 +518,9 @@ TEST(usercmd_range_is_as_good_as_selection)
 	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), sandbox, "", cwd);
 	populate_dir_list(&lwin, 0);
 
-	assert_success(exec_commands("command! ex :normal 777cp", &lwin,
+	assert_success(cmds_dispatch("command! ex :normal 777cp", &lwin,
 				CIT_COMMAND));
-	assert_success(exec_commands("%ex", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("%ex", &lwin, CIT_COMMAND));
 
 	populate_dir_list(&lwin, 1);
 	assert_int_equal(FT_EXEC, lwin.dir_entry[0].type);
@@ -539,7 +541,7 @@ TEST(usercmd_range_is_as_good_as_selection)
 	assert_non_null(flist_custom_add(&lwin, "existing-files/b"));
 	assert_success(flist_custom_finish(&lwin, CV_REGULAR, 0));
 
-	assert_failure(exec_commands(".find a", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch(".find a", &lwin, CIT_COMMAND));
 
 	const char *find_lines[] = { "existing-files/a", "a" };
 	file_is(SANDBOX_PATH "/vi-list", find_lines, ARRAY_LEN(find_lines));
@@ -556,5 +558,37 @@ TEST(usercmd_range_is_as_good_as_selection)
 	stats_reset(&cfg);
 }
 
+TEST(search_usercmd_perform_search)
+{
+	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), test_data, "", cwd);
+	populate_dir_list(&lwin, /*reload=*/0);
+	assert_int_equal(0, lwin.list_pos);
+
+	assert_string_equal("", lwin.last_search);
+
+	assert_success(cmds_dispatch("command cmd /var", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cmd", &lwin, CIT_COMMAND));
+
+	assert_int_equal(9, lwin.list_pos);
+	assert_string_equal("var", lwin.last_search);
+}
+
+TEST(search_usercmd_sets_search_direction_on_editing)
+{
+	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), test_data, "", cwd);
+	populate_dir_list(&lwin, /*reload=*/0);
+	assert_int_equal(0, lwin.list_pos);
+
+	assert_string_equal("", lwin.last_search);
+	curr_stats.last_search_backward = 1;
+
+	assert_success(cmds_dispatch("command cmd /var", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cmd", &lwin, CIT_COMMAND));
+
+	assert_int_equal(9, lwin.list_pos);
+	assert_string_equal("var", lwin.last_search);
+	assert_false(curr_stats.last_search_backward);
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/commands/highlight.c b/tests/commands/highlight.c
index 61fdb2e..3f7d36b 100644
--- a/tests/commands/highlight.c
+++ b/tests/commands/highlight.c
@@ -16,7 +16,7 @@
 
 SETUP_ONCE()
 {
-	init_commands();
+	cmds_init();
 	cs_reset(&cfg.cs);
 	lwin.list_rows = 0;
 	rwin.list_rows = 0;
@@ -41,24 +41,24 @@ TEARDOWN()
 TEST(wrong_gui_color_causes_error)
 {
 	ui_sb_msg("");
-	assert_failure(exec_commands("hi Win guifg=#1234", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("hi Win guifg=#1234", &lwin, CIT_COMMAND));
 	assert_string_equal("Unrecognized color value format: #1234", ui_sb_last());
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("hi Win guibg=#1234", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("hi Win guibg=#1234", &lwin, CIT_COMMAND));
 	assert_string_equal("Unrecognized color value format: #1234", ui_sb_last());
 }
 
 TEST(gui_colors_are_parsed)
 {
-	assert_success(exec_commands("hi Win guifg=#1234fe guibg=red gui=reverse",
+	assert_success(cmds_dispatch("hi Win guifg=#1234fe guibg=red gui=reverse",
 				&lwin, CIT_COMMAND));
 	assert_true(curr_stats.cs->color[WIN_COLOR].gui_set);
 	assert_int_equal(0x1234fe, curr_stats.cs->color[WIN_COLOR].gui_fg);
 	assert_int_equal(COLOR_RED, curr_stats.cs->color[WIN_COLOR].gui_bg);
 	assert_int_equal(A_REVERSE, curr_stats.cs->color[WIN_COLOR].gui_attr);
 
-	assert_success(exec_commands("hi Win guifg=default", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("hi Win guifg=default", &lwin, CIT_COMMAND));
 	assert_true(curr_stats.cs->color[WIN_COLOR].gui_set);
 	assert_int_equal(-1, curr_stats.cs->color[WIN_COLOR].gui_fg);
 	assert_int_equal(COLOR_RED, curr_stats.cs->color[WIN_COLOR].gui_bg);
@@ -68,9 +68,9 @@ TEST(gui_colors_are_parsed)
 TEST(gui_colors_are_printed)
 {
 	ui_sb_msg("");
-	assert_success(exec_commands("hi Win guifg=#1234fe guibg=red", &lwin,
+	assert_success(cmds_dispatch("hi Win guifg=#1234fe guibg=red", &lwin,
 				CIT_COMMAND));
-	assert_failure(exec_commands("hi Win", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("hi Win", &lwin, CIT_COMMAND));
 	assert_string_equal(
 			"Win        cterm=none ctermfg=white   ctermbg=black  \n"
 			"           gui=none   guifg=#1234fe   guibg=red    ",
@@ -81,7 +81,7 @@ TEST(gui_colors_are_printed)
 
 TEST(wrong_attribute_causes_error)
 {
-	assert_failure(exec_commands("hi Win cterm=bad", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("hi Win cterm=bad", &lwin, CIT_COMMAND));
 }
 
 TEST(various_attributes_are_parsed)
@@ -95,15 +95,15 @@ TEST(various_attributes_are_parsed)
 
 	curr_stats.cs->color[WIN_COLOR].attr = 0;
 
-	assert_success(exec_commands("hi Win cterm=bold,italic", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("hi Win cterm=bold,italic", &lwin, CIT_COMMAND));
 	assert_int_equal(A_BOLD | italic_attr, curr_stats.cs->color[WIN_COLOR].attr);
 
-	assert_success(exec_commands("hi Win cterm=underline,reverse", &lwin,
+	assert_success(cmds_dispatch("hi Win cterm=underline,reverse", &lwin,
 				CIT_COMMAND));
 	assert_int_equal(A_UNDERLINE | A_REVERSE,
 			curr_stats.cs->color[WIN_COLOR].attr);
 
-	assert_success(exec_commands("hi Win cterm=standout,combine", &lwin,
+	assert_success(cmds_dispatch("hi Win cterm=standout,combine", &lwin,
 				CIT_COMMAND));
 	assert_int_equal(A_STANDOUT, curr_stats.cs->color[WIN_COLOR].attr);
 	assert_true(curr_stats.cs->color[WIN_COLOR].combine_attrs);
@@ -112,34 +112,34 @@ TEST(various_attributes_are_parsed)
 TEST(attributes_are_printed_back_correctly)
 {
 	ui_sb_msg("");
-	assert_failure(exec_commands("highlight AuxWin", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("highlight AuxWin", &lwin, CIT_COMMAND));
 	assert_string_equal("AuxWin     cterm=none ctermfg=default ctermbg=default",
 			ui_sb_last());
 
-	assert_success(exec_commands("highlight Win cterm=underline,inverse", &lwin,
+	assert_success(cmds_dispatch("highlight Win cterm=underline,inverse", &lwin,
 				CIT_COMMAND));
 
 	ui_sb_msg("");
-	assert_success(exec_commands("highlight AuxWin cterm=combine", &lwin,
+	assert_success(cmds_dispatch("highlight AuxWin cterm=combine", &lwin,
 				CIT_COMMAND));
-	assert_failure(exec_commands("highlight AuxWin", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("highlight AuxWin", &lwin, CIT_COMMAND));
 	assert_string_equal(
 			"AuxWin     cterm=combine ctermfg=default ctermbg=default", ui_sb_last());
 
-	assert_success(exec_commands("highlight Win cterm=underline,inverse", &lwin,
+	assert_success(cmds_dispatch("highlight Win cterm=underline,inverse", &lwin,
 				CIT_COMMAND));
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("highlight Win", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("highlight Win", &lwin, CIT_COMMAND));
 	assert_string_equal(
 			"Win        cterm=underline,reverse ctermfg=white   ctermbg=black  ",
 			ui_sb_last());
 
-	assert_success(exec_commands("highlight Win cterm=italic,standout,bold",
+	assert_success(cmds_dispatch("highlight Win cterm=italic,standout,bold",
 				&lwin, CIT_COMMAND));
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("highlight Win", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("highlight Win", &lwin, CIT_COMMAND));
 #ifdef HAVE_A_ITALIC_DECL
 	assert_string_equal(
 			"Win        cterm=bold,standout,italic ctermfg=white   ctermbg=black  ",
@@ -160,7 +160,7 @@ TEST(color_is_set)
 	curr_stats.cs->color[WIN_COLOR].fg = COLOR_BLUE;
 	curr_stats.cs->color[WIN_COLOR].bg = COLOR_BLUE;
 	curr_stats.cs->color[WIN_COLOR].attr = 0;
-	assert_success(exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_int_equal(COLOR_RED, curr_stats.cs->color[WIN_COLOR].fg);
 	assert_int_equal(COLOR_RED, curr_stats.cs->color[WIN_COLOR].bg);
 	assert_int_equal(A_BOLD, curr_stats.cs->color[WIN_COLOR].attr);
@@ -171,7 +171,7 @@ TEST(original_color_is_unchanged_on_parsing_error)
 	const char *const COMMANDS = "highlight Win ctermfg=red ctersmbg=red";
 
 	curr_stats.cs->color[WIN_COLOR].fg = COLOR_BLUE;
-	assert_failure(exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_int_equal(COLOR_BLUE, curr_stats.cs->color[WIN_COLOR].fg);
 }
 
@@ -181,14 +181,14 @@ TEST(empty_curly_braces)
 {
 	const char *const COMMANDS = "highlight {} ctermfg=red";
 
-	assert_false(exec_commands(COMMANDS, &lwin, CIT_COMMAND) == 0);
+	assert_false(cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND) == 0);
 }
 
 TEST(curly_braces_pattern_transform)
 {
 	const char *const COMMANDS = "highlight {*.sh}<inode/directory> ctermfg=red";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal("{*.sh}<inode/directory>",
 			matchers_get_expr(cfg.cs.file_hi[0].matchers));
 }
@@ -197,28 +197,28 @@ TEST(curly_braces_no_flags_allowed)
 {
 	const char *const COMMANDS = "highlight {*.sh}i ctermfg=red";
 
-	assert_false(exec_commands(COMMANDS, &lwin, CIT_COMMAND) == 0);
+	assert_false(cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND) == 0);
 }
 
 TEST(empty_re_without_flags)
 {
 	const char *const COMMANDS = "highlight // ctermfg=red";
 
-	assert_false(exec_commands(COMMANDS, &lwin, CIT_COMMAND) == 0);
+	assert_false(cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND) == 0);
 }
 
 TEST(empty_re_with_flags)
 {
 	const char *const COMMANDS = "highlight //i ctermfg=red";
 
-	assert_false(exec_commands(COMMANDS, &lwin, CIT_COMMAND) == 0);
+	assert_false(cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND) == 0);
 }
 
 TEST(pattern_is_not_unescaped)
 {
 	const char *const COMMANDS = "highlight /^\\./ ctermfg=red";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal("/^\\./", matchers_get_expr(cfg.cs.file_hi[0].matchers));
 }
 
@@ -228,7 +228,7 @@ TEST(pattern_length_is_not_limited)
 		"|bz2|cab|cpio|deb|gz|jar|lha|lrz|lz|lzma|lzo|rar|rpm|rz|t7z|tZ|tar|tbz"
 		"|tbz2|tgz|tlz|txz|tzo|war|xz|zip)$/ ctermfg=red";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal("/\\.(7z|Z|a|ace|alz|apkg|arc|arj|bz"
 		"|bz2|cab|cpio|deb|gz|jar|lha|lrz|lz|lzma|lzo|rar|rpm|rz|t7z|tZ|tar|tbz"
 		"|tbz2|tgz|tlz|txz|tzo|war|xz|zip)$/",
@@ -239,7 +239,7 @@ TEST(i_flag)
 {
 	const char *const COMMANDS = "highlight /^\\./i ctermfg=red";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal("/^\\./i", matchers_get_expr(cfg.cs.file_hi[0].matchers));
 }
 
@@ -247,7 +247,7 @@ TEST(I_flag)
 {
 	const char *const COMMANDS = "highlight /^\\./I ctermfg=red";
 
-	assert_int_equal(0, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(0, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal("/^\\./I", matchers_get_expr(cfg.cs.file_hi[0].matchers));
 }
 
@@ -255,14 +255,14 @@ TEST(wrong_flag)
 {
 	const char *const COMMANDS = "highlight /^\\./x ctermfg=red";
 
-	assert_int_equal(1, exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_int_equal(-1, cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 }
 
 TEST(negation)
 {
 	const char *const COMMANDS = "highlight !/^\\./i ctermfg=red";
 
-	assert_success(exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_string_equal("!/^\\./i",
 			matchers_get_expr(cfg.cs.file_hi[0].matchers));
 }
@@ -270,10 +270,10 @@ TEST(negation)
 TEST(highlighting_is_printed_back_correctly)
 {
 	const char *const COMMANDS = "highlight {*.jpg} ctermfg=red";
-	assert_success(exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("highlight {*.jpg}", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("highlight {*.jpg}", &lwin, CIT_COMMAND));
 	assert_string_equal("{*.jpg}    cterm=none ctermfg=red     ctermbg=default",
 			ui_sb_last());
 }
@@ -283,8 +283,8 @@ TEST(existing_records_are_updated)
 	const char *const COMMANDS1 = "highlight {*.jpg} ctermfg=red";
 	const char *const COMMANDS2 = "highlight {*.jpg} ctermfg=blue";
 
-	assert_success(exec_commands(COMMANDS1, &lwin, CIT_COMMAND));
-	assert_success(exec_commands(COMMANDS2, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(COMMANDS1, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(COMMANDS2, &lwin, CIT_COMMAND));
 	assert_int_equal(1, cfg.cs.file_hi_count);
 
 	assert_int_equal(COLOR_BLUE, cfg.cs.file_hi[0].hi.fg);
@@ -296,10 +296,10 @@ TEST(all_records_can_be_removed)
 	const char *const COMMANDS2 = "highlight {*.avi} cterm=bold";
 	const char *const COMMANDS3 = "highlight clear";
 
-	assert_success(exec_commands(COMMANDS1, &lwin, CIT_COMMAND));
-	assert_success(exec_commands(COMMANDS2, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(COMMANDS1, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(COMMANDS2, &lwin, CIT_COMMAND));
 	assert_int_equal(2, cfg.cs.file_hi_count);
-	assert_success(exec_commands(COMMANDS3, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(COMMANDS3, &lwin, CIT_COMMAND));
 	assert_int_equal(0, cfg.cs.file_hi_count);
 }
 
@@ -309,18 +309,18 @@ TEST(records_can_be_removed)
 	const char *const COMMANDS2 = "highlight clear {*.avi}";
 	const char *const COMMANDS3 = "highlight clear {*.jpg}";
 
-	assert_success(exec_commands(COMMANDS1, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(COMMANDS1, &lwin, CIT_COMMAND));
 	assert_int_equal(1, cfg.cs.file_hi_count);
-	assert_failure(exec_commands(COMMANDS2, &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch(COMMANDS2, &lwin, CIT_COMMAND));
 	assert_int_equal(1, cfg.cs.file_hi_count);
-	assert_success(exec_commands(COMMANDS3, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(COMMANDS3, &lwin, CIT_COMMAND));
 	assert_int_equal(0, cfg.cs.file_hi_count);
 }
 
 TEST(incorrect_highlight_groups_are_not_added)
 {
 	const char *const COMMANDS = "highlight {*.jpg} ctersmfg=red";
-	assert_failure(exec_commands(COMMANDS, &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch(COMMANDS, &lwin, CIT_COMMAND));
 	assert_int_equal(0, cfg.cs.file_hi_count);
 }
 
@@ -339,7 +339,7 @@ TEST(can_color_uncolored_file)
 				&lwin.dir_entry[0].hi_num));
 	assert_int_equal(INT_MAX, lwin.dir_entry[0].hi_num);
 
-	assert_success(exec_commands("highlight {*.vifm} cterm=bold", &lwin,
+	assert_success(cmds_dispatch("highlight {*.vifm} cterm=bold", &lwin,
 				CIT_COMMAND));
 	assert_non_null(cs_get_file_hi(curr_stats.cs, "some.vifm",
 				&lwin.dir_entry[0].hi_num));
@@ -355,11 +355,11 @@ TEST(tabs_are_allowed)
 	const char *const COMMANDS2 = "highlight {*.avi}\tctermfg=red";
 	const char *const COMMANDS3 = "highlight\t{*.mp3}\tctermfg=red";
 
-	assert_success(exec_commands(COMMANDS1, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(COMMANDS1, &lwin, CIT_COMMAND));
 	assert_int_equal(1, cfg.cs.file_hi_count);
-	assert_success(exec_commands(COMMANDS2, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(COMMANDS2, &lwin, CIT_COMMAND));
 	assert_int_equal(2, cfg.cs.file_hi_count);
-	assert_success(exec_commands(COMMANDS3, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(COMMANDS3, &lwin, CIT_COMMAND));
 	assert_int_equal(3, cfg.cs.file_hi_count);
 
 	if(cfg.cs.file_hi_count > 0)
diff --git a/tests/commands/map.c b/tests/commands/map.c
index 3c9797b..684b259 100644
--- a/tests/commands/map.c
+++ b/tests/commands/map.c
@@ -14,8 +14,8 @@ static int silence;
 
 SETUP()
 {
-	init_modes();
-	init_commands();
+	modes_init();
+	cmds_init();
 
 	curr_view = &lwin;
 	other_view = &rwin;
@@ -31,30 +31,30 @@ TEST(map_commands_count_arguments_correctly)
 {
 	/* Each map command below should receive two arguments: "\\" and "j". */
 	/* Each unmap command below should receive single argument: "\\". */
-	assert_success(exec_commands("cmap \\ j", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("cnoremap \\ j", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("cunmap \\", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("dmap \\ j", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("dnoremap \\ j", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("dunmap \\", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("mmap \\ j", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("mnoremap \\ j", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("munmap \\", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("nmap \\ j", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("nnoremap \\ j", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("nunmap \\", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("map \\ j", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("noremap \\ j", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("unmap \\", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("map! \\ j", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("noremap! \\ j", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("unmap! \\", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("qmap \\ j", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("qnoremap \\ j", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("qunmap \\", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("vmap \\ j", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("vnoremap \\ j", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("vunmap \\", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cmap \\ j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cnoremap \\ j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cunmap \\", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("dmap \\ j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("dnoremap \\ j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("dunmap \\", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("mmap \\ j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("mnoremap \\ j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("munmap \\", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nmap \\ j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nnoremap \\ j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nunmap \\", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("map \\ j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("noremap \\ j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("unmap \\", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("map! \\ j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("noremap! \\ j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("unmap! \\", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("qmap \\ j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("qnoremap \\ j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("qunmap \\", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("vmap \\ j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("vnoremap \\ j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("vunmap \\", &lwin, CIT_COMMAND));
 }
 
 TEST(map_parses_args)
@@ -73,10 +73,10 @@ TEST(map_parses_args)
 
 	/* <silent> */
 	assert_int_equal(0, silence);
-	assert_success(exec_commands("map a x", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("map <silent>b x", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("map <silent> c x", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("map <silent><silent>d x", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("map a x", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("map <silent>b x", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("map <silent> c x", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("map <silent><silent>d x", &lwin, CIT_COMMAND));
 	assert_false(IS_KEYS_RET_CODE(vle_keys_exec(L"a")));
 	assert_false(IS_KEYS_RET_CODE(vle_keys_exec(L"1b")));
 	assert_false(IS_KEYS_RET_CODE(vle_keys_exec(L"1c")));
@@ -84,14 +84,14 @@ TEST(map_parses_args)
 	assert_int_equal(0, silence);
 
 	/* <wait> */
-	assert_success(exec_commands("map <wait>xj j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("map <wait>xj j", &lwin, CIT_COMMAND));
 	assert_int_equal(KEYS_WAIT, vle_keys_exec(L"x"));
 }
 
 TEST(dialogs_exit_silent_mode)
 {
 	const char *cmd = "map <silent> b :cd /no-such-dir<cr>";
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 
 	curr_stats.load_stage = -1;
 	stats_silence_ui(1);
diff --git a/tests/commands/misc.c b/tests/commands/misc.c
index 37cbeab..e0a3692 100644
--- a/tests/commands/misc.c
+++ b/tests/commands/misc.c
@@ -1,10 +1,9 @@
 #include <stic.h>
 
-#include <sys/stat.h> /* chmod() */
-#include <unistd.h> /* F_OK access() chdir() rmdir() symlink() unlink() */
+#include <unistd.h> /* chdir() rmdir() symlink() unlink() */
 
 #include <limits.h> /* INT_MAX */
-#include <stdio.h> /* FILE fclose() fopen() fprintf() remove() */
+#include <stdio.h> /* remove() */
 #include <string.h> /* strcpy() strdup() */
 
 #include <test-utils.h>
@@ -24,10 +23,13 @@
 #include "../../src/utils/str.h"
 #include "../../src/utils/string_array.h"
 #include "../../src/cmd_core.h"
+#include "../../src/compare.h"
 #include "../../src/filelist.h"
 #include "../../src/flist_hist.h"
 #include "../../src/plugins.h"
 #include "../../src/registers.h"
+#include "../../src/running.h"
+#include "../../src/status.h"
 
 static char *saved_cwd;
 
@@ -40,7 +42,6 @@ static void strings_list_is(const strlist_t expected, const strlist_t actual);
 SETUP_ONCE()
 {
 	cfg.sizefmt.base = 1;
-	cfg.dot_dirs = DD_TREE_LEAFS_PARENT;
 
 	assert_non_null(get_cwd(cwd, sizeof(cwd)));
 
@@ -56,24 +57,9 @@ SETUP()
 	curr_view = &lwin;
 	other_view = &rwin;
 
-	cfg.cd_path = strdup("");
-	cfg.fuse_home = strdup("");
-	cfg.slow_fs_list = strdup("");
-	cfg.use_system_calls = 1;
-
-#ifndef _WIN32
-	replace_string(&cfg.shell, "/bin/sh");
-	update_string(&cfg.shell_cmd_flag, "-c");
-#else
-	replace_string(&cfg.shell, "cmd");
-	update_string(&cfg.shell_cmd_flag, "/C");
-#endif
-
-	stats_update_shell_type(cfg.shell);
-
-	init_commands();
-
+	conf_setup();
 	undo_setup();
+	cmds_init();
 
 	saved_cwd = save_cwd();
 }
@@ -82,19 +68,11 @@ TEARDOWN()
 {
 	restore_cwd(saved_cwd);
 
-	update_string(&cfg.cd_path, NULL);
-	update_string(&cfg.fuse_home, NULL);
-	update_string(&cfg.slow_fs_list, NULL);
-
-	stats_update_shell_type("/bin/sh");
-	update_string(&cfg.shell_cmd_flag, NULL);
-	update_string(&cfg.shell, NULL);
-
 	view_teardown(&lwin);
 	view_teardown(&rwin);
 
+	conf_teardown();
 	vle_cmds_reset();
-
 	undo_teardown();
 }
 
@@ -105,7 +83,7 @@ TEST(cd_in_root_works)
 	strcpy(lwin.curr_dir, test_data);
 
 	assert_false(is_root_dir(lwin.curr_dir));
-	assert_success(exec_commands("cd /", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cd /", &lwin, CIT_COMMAND));
 	assert_true(is_root_dir(lwin.curr_dir));
 }
 
@@ -118,7 +96,7 @@ TEST(double_cd_uses_same_base_for_rel_paths)
 	strcpy(lwin.curr_dir, test_data);
 	strcpy(rwin.curr_dir, "..");
 
-	assert_success(exec_commands("cd read rename", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cd read rename", &lwin, CIT_COMMAND));
 
 	snprintf(path, sizeof(path), "%s/read", test_data);
 	assert_true(paths_are_equal(lwin.curr_dir, path));
@@ -144,7 +122,7 @@ TEST(tr_extends_second_field)
 	lwin.dir_entry[0].name = strdup("a b");
 	lwin.dir_entry[0].origin = &lwin.curr_dir[0];
 
-	(void)exec_commands("tr/ ?<>\\\\:*|\"/_", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("tr/ ?<>\\\\:*|\"/_", &lwin, CIT_COMMAND);
 
 	snprintf(path, sizeof(path), "%s/a_b", sandbox);
 	assert_success(remove(path));
@@ -172,7 +150,7 @@ TEST(substitute_works)
 	lwin.dir_entry[1].name = strdup("B c");
 	lwin.dir_entry[1].origin = &lwin.curr_dir[0];
 
-	(void)exec_commands("%substitute/b/c/Iig", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("%substitute/b/c/Iig", &lwin, CIT_COMMAND);
 
 	snprintf(path, sizeof(path), "%s/a c c", sandbox);
 	assert_success(remove(path));
@@ -202,7 +180,7 @@ TEST(chmod_works, IF(not_windows))
 	lwin.dir_entry[1].name = strdup("file2");
 	lwin.dir_entry[1].origin = &lwin.curr_dir[0];
 
-	(void)exec_commands("1,2chmod +x", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("1,2chmod +x", &lwin, CIT_COMMAND);
 
 	populate_dir_list(&lwin, 1);
 	assert_int_equal(FT_EXEC, lwin.dir_entry[0].type);
@@ -214,43 +192,6 @@ TEST(chmod_works, IF(not_windows))
 	assert_success(remove(path));
 }
 
-TEST(edit_handles_ranges, IF(not_windows))
-{
-	create_file(SANDBOX_PATH "/file1");
-	create_file(SANDBOX_PATH "/file2");
-
-	char script_path[PATH_MAX + 1];
-	make_abs_path(script_path, sizeof(script_path), sandbox, "script", NULL);
-	update_string(&cfg.vi_command, script_path);
-	update_string(&cfg.vi_x_command, "");
-
-	FILE *fp = fopen(SANDBOX_PATH "/script", "w");
-	fprintf(fp, "#!/bin/sh\n");
-	fprintf(fp, "for arg; do echo \"$arg\" >> %s/vi-list; done\n", SANDBOX_PATH);
-	fclose(fp);
-	assert_success(chmod(SANDBOX_PATH "/script", 0777));
-
-	strcpy(lwin.curr_dir, sandbox);
-	lwin.list_rows = 2;
-	lwin.list_pos = 0;
-	lwin.dir_entry = dynarray_cextend(NULL,
-			lwin.list_rows*sizeof(*lwin.dir_entry));
-	lwin.dir_entry[0].name = strdup("file1");
-	lwin.dir_entry[0].origin = &lwin.curr_dir[0];
-	lwin.dir_entry[1].name = strdup("file2");
-	lwin.dir_entry[1].origin = &lwin.curr_dir[0];
-
-	(void)exec_commands("%edit", &lwin, CIT_COMMAND);
-
-	const char *lines[] = { "file1", "file2" };
-	file_is(SANDBOX_PATH "/vi-list", lines, ARRAY_LEN(lines));
-
-	assert_success(remove(SANDBOX_PATH "/script"));
-	assert_success(remove(SANDBOX_PATH "/file1"));
-	assert_success(remove(SANDBOX_PATH "/file2"));
-	assert_success(remove(SANDBOX_PATH "/vi-list"));
-}
-
 TEST(putting_files_works)
 {
 	char path[PATH_MAX + 1];
@@ -264,7 +205,7 @@ TEST(putting_files_works)
 	assert_success(regs_append(DEFAULT_REG_NAME, path));
 	lwin.list_pos = 1;
 
-	assert_true(exec_commands("put", &lwin, CIT_COMMAND) != 0);
+	assert_true(cmds_dispatch("put", &lwin, CIT_COMMAND) != 0);
 	restore_cwd(saved_cwd);
 	saved_cwd = save_cwd();
 
@@ -277,7 +218,7 @@ TEST(putting_files_works)
 TEST(yank_works_with_ranges)
 {
 	char path[PATH_MAX + 1];
-	reg_t *reg;
+	const reg_t *reg;
 
 	regs_init();
 
@@ -290,7 +231,7 @@ TEST(yank_works_with_ranges)
 	assert_non_null(reg);
 
 	assert_int_equal(0, reg->nfiles);
-	(void)exec_commands("%yank", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("%yank", &lwin, CIT_COMMAND);
 	assert_int_equal(1, reg->nfiles);
 
 	regs_reset();
@@ -322,7 +263,7 @@ TEST(symlinks_in_paths_are_not_resolved, IF(not_windows))
 			sizeof(canonic_path));
 
 	/* :mkdir */
-	(void)exec_commands("mkdir ../dir", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("mkdir ../dir", &lwin, CIT_COMMAND);
 	restore_cwd(saved_cwd);
 	saved_cwd = save_cwd();
 	assert_success(rmdir(SANDBOX_PATH "/dir"));
@@ -330,7 +271,7 @@ TEST(symlinks_in_paths_are_not_resolved, IF(not_windows))
 	/* :clone file name. */
 	create_file(SANDBOX_PATH "/dir-link/file");
 	populate_dir_list(&lwin, 1);
-	(void)exec_commands("clone ../file-clone", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("clone ../file-clone", &lwin, CIT_COMMAND);
 	restore_cwd(saved_cwd);
 	saved_cwd = save_cwd();
 	assert_success(remove(SANDBOX_PATH "/file-clone"));
@@ -341,11 +282,11 @@ TEST(symlinks_in_paths_are_not_resolved, IF(not_windows))
 			"scripts/", saved_cwd);
 	snprintf(buf, sizeof(buf), "colorscheme set-env %s/../dir-link/..",
 			sandbox);
-	assert_success(exec_commands(buf, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(buf, &lwin, CIT_COMMAND));
 	cs_load_defaults();
 
 	/* :cd */
-	assert_success(exec_commands("cd ../dir-link/..", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("cd ../dir-link/..", &lwin, CIT_COMMAND));
 	assert_string_equal(canonic_path, lwin.curr_dir);
 
 	restore_cwd(saved_cwd);
@@ -365,18 +306,18 @@ TEST(grep_command, IF(not_windows))
 	assert_success(chdir(TEST_DATA_PATH "/scripts"));
 	assert_non_null(get_cwd(lwin.curr_dir, sizeof(lwin.curr_dir)));
 
-	assert_success(exec_commands("set grepprg='grep -n -H -r %i %a %s %u'", &lwin,
+	assert_success(cmds_dispatch("set grepprg='grep -n -H -r %i %a %s %u'", &lwin,
 				CIT_COMMAND));
 
 	/* Nothing to repeat. */
-	assert_failure(exec_commands("grep", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("grep", &lwin, CIT_COMMAND));
 
-	assert_success(exec_commands("grep command", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("grep command", &lwin, CIT_COMMAND));
 	assert_int_equal(2, lwin.list_rows);
 	assert_string_equal("Grep command", lwin.custom.title);
 
 	/* Repeat last grep, but add inversion. */
-	assert_success(exec_commands("grep!", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("grep!", &lwin, CIT_COMMAND));
 	assert_int_equal(5, lwin.list_rows);
 	assert_string_equal("Grep command", lwin.custom.title);
 
@@ -386,7 +327,7 @@ TEST(grep_command, IF(not_windows))
 TEST(touch)
 {
 	to_canonic_path(SANDBOX_PATH, cwd, lwin.curr_dir, sizeof(lwin.curr_dir));
-	(void)exec_commands("touch file", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("touch file", &lwin, CIT_COMMAND);
 
 	assert_success(remove(SANDBOX_PATH "/file"));
 }
@@ -394,22 +335,58 @@ TEST(touch)
 TEST(compare)
 {
 	opt_handlers_setup();
-
 	create_file(SANDBOX_PATH "/file");
 
 	to_canonic_path(SANDBOX_PATH, cwd, lwin.curr_dir, sizeof(lwin.curr_dir));
 
 	/* The file is empty so nothing to do when "skipempty" is specified. */
-	assert_success(exec_commands("compare ofone skipempty", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("compare ofone skipempty", &lwin, CIT_COMMAND));
 	assert_false(flist_custom_active(&lwin));
 
-	(void)exec_commands("compare byname bysize bycontents listall listdups "
+	/* Verify that later arguments override the former ones. */
+	(void)cmds_dispatch("compare byname bysize bycontents listall listdups "
 			"listunique ofboth ofone groupids grouppaths", &lwin, CIT_COMMAND);
 	assert_true(flist_custom_active(&lwin));
 	assert_int_equal(CV_REGULAR, lwin.custom.type);
-
+	rn_leave(&lwin, /*levels=*/1);
+
+	/* Can't toggle without !. */
+	(void)cmds_dispatch("compare byname", &lwin, CIT_COMMAND);
+	assert_int_equal(CF_GROUP_PATHS | CF_SHOW, lwin.custom.diff_cmp_flags);
+	(void)cmds_dispatch("compare showdifferent", &lwin, CIT_COMMAND);
+	assert_int_equal(CF_GROUP_PATHS | CF_SHOW, lwin.custom.diff_cmp_flags);
+	rn_leave(&lwin, /*levels=*/1);
+
+	/* No toggling. */
+	(void)cmds_dispatch("compare! showdifferent", &lwin, CIT_COMMAND);
+	assert_string_equal("Toggling requires active compare view", ui_sb_last());
+	/* Verify that two-pane compare gets correct arguments. */
+	make_abs_path(rwin.curr_dir, sizeof(rwin.curr_dir), TEST_DATA_PATH, "rename",
+			cwd);
+	(void)cmds_dispatch("compare byname withrcase withicase", &lwin, CIT_COMMAND);
+	assert_true(flist_custom_active(&lwin));
+	assert_true(flist_custom_active(&rwin));
+	assert_int_equal(CT_NAME, lwin.custom.diff_cmp_type);
+	assert_int_equal(LT_ALL, lwin.custom.diff_list_type);
+	assert_int_equal(CF_GROUP_PATHS | CF_IGNORE_CASE | CF_SHOW,
+			lwin.custom.diff_cmp_flags);
+	/* Toggling. */
+	(void)cmds_dispatch("compare! showidentical showdifferent", &lwin,
+			CIT_COMMAND);
+	assert_true(flist_custom_active(&lwin));
+	assert_true(flist_custom_active(&rwin));
+	assert_int_equal(CT_NAME, lwin.custom.diff_cmp_type);
+	assert_int_equal(LT_ALL, lwin.custom.diff_list_type);
+	assert_int_equal(CF_GROUP_PATHS | CF_IGNORE_CASE | CF_SHOW_UNIQUE_LEFT |
+			CF_SHOW_UNIQUE_RIGHT, lwin.custom.diff_cmp_flags);
+	/* Bad toggling. */
+	(void)cmds_dispatch("compare! byname", &lwin, CIT_COMMAND);
+	assert_int_equal(CF_GROUP_PATHS | CF_IGNORE_CASE | CF_SHOW_UNIQUE_LEFT |
+			CF_SHOW_UNIQUE_RIGHT, lwin.custom.diff_cmp_flags);
+	assert_string_equal("Unexpected property for toggling: byname", ui_sb_last());
+
+	assert_success(chdir(cwd));
 	assert_success(remove(SANDBOX_PATH "/file"));
-
 	opt_handlers_teardown();
 }
 
@@ -418,15 +395,15 @@ TEST(screen)
 	assert_false(cfg.use_term_multiplexer);
 
 	/* :screen toggles the option. */
-	assert_success(exec_commands("screen", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("screen", &lwin, CIT_COMMAND));
 	assert_true(cfg.use_term_multiplexer);
-	assert_success(exec_commands("screen", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("screen", &lwin, CIT_COMMAND));
 	assert_false(cfg.use_term_multiplexer);
 
 	/* :screen! sets it to on. */
-	assert_success(exec_commands("screen!", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("screen!", &lwin, CIT_COMMAND));
 	assert_true(cfg.use_term_multiplexer);
-	assert_success(exec_commands("screen!", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("screen!", &lwin, CIT_COMMAND));
 	assert_true(cfg.use_term_multiplexer);
 
 	cfg.use_term_multiplexer = 0;
@@ -443,9 +420,9 @@ TEST(hist_next_and_prev)
 	flist_hist_setup(&lwin, sandbox, ".", 0, 1);
 	flist_hist_setup(&lwin, test_data, ".", 0, 1);
 
-	assert_success(exec_commands("histprev", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("histprev", &lwin, CIT_COMMAND));
 	assert_true(paths_are_same(lwin.curr_dir, sandbox));
-	assert_success(exec_commands("histnext", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("histnext", &lwin, CIT_COMMAND));
 	assert_true(paths_are_same(lwin.curr_dir, test_data));
 
 	cfg_resize_histories(0);
@@ -453,7 +430,7 @@ TEST(hist_next_and_prev)
 
 TEST(normal_command_does_not_reset_selection)
 {
-	init_modes();
+	modes_init();
 	opt_handlers_setup();
 
 	lwin.list_rows = 2;
@@ -468,17 +445,17 @@ TEST(normal_command_does_not_reset_selection)
 	lwin.dir_entry[1].selected = 0;
 	lwin.selected_files = 1;
 
-	assert_success(exec_commands(":normal! t", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(":normal! t", &lwin, CIT_COMMAND));
 	assert_int_equal(0, lwin.selected_files);
 	assert_false(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
 
-	assert_success(exec_commands(":normal! vG\r", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(":normal! vG\r", &lwin, CIT_COMMAND));
 	assert_int_equal(2, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_true(lwin.dir_entry[1].selected);
 
-	assert_success(exec_commands(":normal! t", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(":normal! t", &lwin, CIT_COMMAND));
 	assert_int_equal(1, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
@@ -493,29 +470,29 @@ TEST(keepsel_preserves_selection)
 
 	lwin.dir_entry[0].selected = 1;
 	lwin.selected_files = 1;
-	assert_failure(exec_commands("echo 'hi'", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("echo 'hi'", &lwin, CIT_COMMAND));
 	assert_int_equal(0, lwin.selected_files);
 	assert_false(lwin.dir_entry[0].selected);
 
 	lwin.dir_entry[0].selected = 1;
 	lwin.selected_files = 1;
-	assert_failure(exec_commands("keepsel echo 'hi'", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("keepsel echo 'hi'", &lwin, CIT_COMMAND));
 	assert_int_equal(1, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 }
 
 TEST(goto_command)
 {
-	assert_failure(exec_commands("goto /", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("goto /no-such-path", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("goto /", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("goto /no-such-path", &lwin, CIT_COMMAND));
 
 	char cmd[PATH_MAX*2];
 	snprintf(cmd, sizeof(cmd), "goto %s/compare", test_data);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 	assert_true(paths_are_same(lwin.curr_dir, test_data));
 	assert_string_equal("compare", get_current_file_name(&lwin));
 
-	assert_success(exec_commands("goto tree", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("goto tree", &lwin, CIT_COMMAND));
 	assert_true(paths_are_same(lwin.curr_dir, test_data));
 	assert_string_equal("tree", get_current_file_name(&lwin));
 }
@@ -524,7 +501,7 @@ TEST(goto_normalizes_slashes, IF(windows))
 {
 	char cmd[PATH_MAX*2];
 	snprintf(cmd, sizeof(cmd), "goto %s\\\\compare", test_data);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
 	assert_true(paths_are_same(lwin.curr_dir, test_data));
 	assert_string_equal("compare", get_current_file_name(&lwin));
 }
@@ -537,14 +514,14 @@ TEST(echo_reports_all_errors)
 	           "Invalid expression: \"hi";
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("echo \"hi", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("echo \"hi", &lwin, CIT_COMMAND));
 	assert_string_equal(expected, ui_sb_last());
 
 	expected = "Expression is missing closing parenthesis: (1\n"
 	           "Invalid expression: (1";
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("echo (1", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("echo (1", &lwin, CIT_COMMAND));
 	assert_string_equal(expected, ui_sb_last());
 
 	char zeroes[8192] = "echo ";
@@ -552,20 +529,33 @@ TEST(echo_reports_all_errors)
 	zeroes[sizeof(zeroes) - 1U] = '\0';
 
 	ui_sb_msg("");
-	assert_failure(exec_commands(zeroes, &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch(zeroes, &lwin, CIT_COMMAND));
 	assert_true(strchr(ui_sb_last(), '\n') != NULL);
 }
 
+TEST(echo_without_arguments_prints_nothing)
+{
+	ui_sb_msg("");
+
+	/* First, print some message to record it as the last one. */
+	assert_failure(cmds_dispatch("echo 'previous'", &lwin, CIT_COMMAND));
+	assert_string_equal("previous", ui_sb_last());
+
+	/* Now, no message.  The last one could popup here. */
+	assert_failure(cmds_dispatch("echo", &lwin, CIT_COMMAND));
+	assert_string_equal("", ui_sb_last());
+}
+
 TEST(zero_count_is_rejected)
 {
 	const char *expected = "Count argument can't be zero";
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("delete a 0", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("delete a 0", &lwin, CIT_COMMAND));
 	assert_string_equal(expected, ui_sb_last());
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("yank a 0", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("yank a 0", &lwin, CIT_COMMAND));
 	assert_string_equal(expected, ui_sb_last());
 }
 
@@ -574,29 +564,29 @@ TEST(tree_command)
 	strcpy(lwin.curr_dir, sandbox);
 
 	/* Invalid input. */
-	assert_failure(exec_commands("tree nesting=0", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("tree nesting=0", &lwin, CIT_COMMAND));
 	assert_false(flist_custom_active(&lwin));
 	assert_string_equal("Invalid argument: nesting=0", ui_sb_last());
-	assert_failure(exec_commands("tree depth=0", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("tree depth=0", &lwin, CIT_COMMAND));
 	assert_false(flist_custom_active(&lwin));
 	assert_string_equal("Invalid depth: 0", ui_sb_last());
 
 	/* :tree enters tree mode. */
-	assert_success(exec_commands("tree", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tree", &lwin, CIT_COMMAND));
 	assert_true(flist_custom_active(&lwin));
 	assert_true(cv_tree(lwin.custom.type));
 
 	/* Repeating :tree leaves view in tree mode. */
-	assert_success(exec_commands("tree", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tree", &lwin, CIT_COMMAND));
 	assert_true(flist_custom_active(&lwin));
 	assert_true(cv_tree(lwin.custom.type));
 
 	/* :tree! can leave tree mode. */
-	assert_success(exec_commands("tree!", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tree!", &lwin, CIT_COMMAND));
 	assert_false(flist_custom_active(&lwin));
 
 	/* :tree! can enter tree mode. */
-	assert_success(exec_commands("tree!", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tree!", &lwin, CIT_COMMAND));
 	assert_true(flist_custom_active(&lwin));
 	assert_true(cv_tree(lwin.custom.type));
 
@@ -610,7 +600,7 @@ TEST(tree_command)
 	snprintf(sub_sub_path, sizeof(sub_sub_path), "%s/sub/sub", sandbox);
 	create_dir(sub_sub_path);
 
-	assert_success(exec_commands("tree depth=1", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tree depth=1", &lwin, CIT_COMMAND));
 	assert_true(flist_custom_active(&lwin));
 	assert_true(cv_tree(lwin.custom.type));
 	assert_int_equal(1, lwin.list_rows);
@@ -624,16 +614,16 @@ TEST(regular_command)
 	strcpy(lwin.curr_dir, sandbox);
 
 	/* :tree enters tree mode. */
-	assert_success(exec_commands("tree", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tree", &lwin, CIT_COMMAND));
 	assert_true(flist_custom_active(&lwin));
 	assert_true(cv_tree(lwin.custom.type));
 
 	/* :regular leaves tree mode. */
-	assert_success(exec_commands("regular", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("regular", &lwin, CIT_COMMAND));
 	assert_false(flist_custom_active(&lwin));
 
 	/* Repeated :regular does nothing. */
-	assert_success(exec_commands("regular", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("regular", &lwin, CIT_COMMAND));
 	assert_false(flist_custom_active(&lwin));
 }
 
@@ -643,29 +633,29 @@ TEST(plugin_command)
 	curr_stats.plugs = plugs_create(curr_stats.vlua);
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("plugin load all", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("plugin load all", &lwin, CIT_COMMAND));
 	assert_string_equal("Trailing characters", ui_sb_last());
-	assert_failure(exec_commands("plugin wrong arg", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("plugin wrong arg", &lwin, CIT_COMMAND));
 	assert_string_equal("Unknown subcommand: wrong", ui_sb_last());
-	assert_failure(exec_commands("plugin args-count", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("plugin args-count", &lwin, CIT_COMMAND));
 	assert_string_equal("Too few arguments", ui_sb_last());
 
-	assert_success(exec_commands("plugin load", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("plugin load", &lwin, CIT_COMMAND));
 
 	strlist_t empty_list = {};
 	char *plug_items[] = { "plug" };
 	strlist_t plug_list = { .items = plug_items, .nitems = 1 };
 
 	ui_sb_msg("");
-	assert_success(exec_commands("plugin blacklist plug", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("plugin blacklist plug", &lwin, CIT_COMMAND));
 	assert_string_equal("", ui_sb_last());
 
 	strings_list_is(plug_list, plugs_get_blacklist(curr_stats.plugs));
 	strings_list_is(empty_list, plugs_get_whitelist(curr_stats.plugs));
 
 	ui_sb_msg("");
-	assert_success(exec_commands("plugin whitelist plug", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("plugin whitelist plug", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("plugin whitelist plug", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("plugin whitelist plug", &lwin, CIT_COMMAND));
 	assert_string_equal("", ui_sb_last());
 
 	strings_list_is(plug_list, plugs_get_blacklist(curr_stats.plugs));
@@ -692,7 +682,7 @@ TEST(help_command)
 
 	cfg.use_vim_help = 0;
 
-	assert_success(exec_commands("help", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("help", &lwin, CIT_COMMAND));
 
 	assert_success(vlua_run_string(curr_stats.vlua, "print(ginfo.action)"));
 	assert_string_equal("edit-one", ui_sb_last());
@@ -707,14 +697,14 @@ TEST(help_command)
 
 	cfg.use_vim_help = 1;
 
-	assert_success(exec_commands("help", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("help", &lwin, CIT_COMMAND));
 
 	assert_success(vlua_run_string(curr_stats.vlua, "print(ginfo.action)"));
 	assert_string_equal("open-help", ui_sb_last());
 	assert_success(vlua_run_string(curr_stats.vlua, "print(ginfo.topic)"));
 	assert_string_equal("vifm-app.txt", ui_sb_last());
 	assert_success(vlua_run_string(curr_stats.vlua, "print(ginfo.vimdocdir)"));
-	assert_true(ends_with(ui_sb_last(), "/vim-doc"));
+	assert_string_ends_with("/vim-doc", ui_sb_last());
 
 	cfg.use_vim_help = 0;
 
@@ -722,63 +712,25 @@ TEST(help_command)
 	curr_stats.vlua = NULL;
 }
 
-TEST(edit_command)
-{
-	curr_stats.exec_env_type = EET_EMULATOR;
-	update_string(&cfg.vi_command, "#vifmtest#editor");
-	cfg.config_dir[0] = '\0';
-
-	curr_stats.vlua = vlua_init();
-
-	assert_success(vlua_run_string(curr_stats.vlua,
-				"function handler(info)"
-				"  local s = ginfo ~= nil"
-				"  ginfo = info"
-				"  return { success = s }"
-				"end"));
-	assert_success(vlua_run_string(curr_stats.vlua,
-				"vifm.addhandler{ name = 'editor', handler = handler }"));
-
-	int i;
-	for(i = 0; i < 2; ++i)
-	{
-		assert_success(exec_commands("edit a b", &lwin, CIT_COMMAND));
-
-		assert_success(vlua_run_string(curr_stats.vlua, "print(ginfo.action)"));
-		assert_string_equal("edit-many", ui_sb_last());
-		assert_success(vlua_run_string(curr_stats.vlua, "print(#ginfo.paths)"));
-		assert_string_equal("2", ui_sb_last());
-		assert_success(vlua_run_string(curr_stats.vlua, "print(ginfo.paths[1])"));
-		assert_string_equal("a", ui_sb_last());
-		assert_success(vlua_run_string(curr_stats.vlua, "print(ginfo.paths[2])"));
-		assert_string_equal("b", ui_sb_last());
-
-		assert_success(vlua_run_string(curr_stats.vlua, "ginfo = {}"));
-	}
-
-	vlua_finish(curr_stats.vlua);
-	curr_stats.vlua = NULL;
-}
-
 TEST(view_command)
 {
 	opt_handlers_setup();
 
 	curr_stats.preview.on = 0;
 
-	assert_success(exec_commands("view", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("view", &lwin, CIT_COMMAND));
 	assert_true(curr_stats.preview.on);
 
-	assert_success(exec_commands("view", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("view", &lwin, CIT_COMMAND));
 	assert_false(curr_stats.preview.on);
 
-	assert_success(exec_commands("view!", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("view!", &lwin, CIT_COMMAND));
 	assert_true(curr_stats.preview.on);
 
-	assert_success(exec_commands("view!", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("view!", &lwin, CIT_COMMAND));
 	assert_true(curr_stats.preview.on);
 
-	assert_success(exec_commands("view", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("view", &lwin, CIT_COMMAND));
 	assert_false(curr_stats.preview.on);
 
 	opt_handlers_teardown();
@@ -789,18 +741,27 @@ TEST(invert_command)
 	opt_handlers_setup();
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("set sort? sortorder?", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set sort? sortorder?", &lwin, CIT_COMMAND));
 	assert_string_equal("  sort=+name\n  sortorder=ascending", ui_sb_last());
 
-	assert_success(exec_commands("invert o", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("invert o", &lwin, CIT_COMMAND));
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("set sort? sortorder?", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set sort? sortorder?", &lwin, CIT_COMMAND));
 	assert_string_equal("  sort=-name\n  sortorder=descending", ui_sb_last());
 
 	opt_handlers_teardown();
 }
 
+TEST(locate_command)
+{
+	ui_sb_msg("");
+
+	/* Nothing to repeat. */
+	assert_failure(cmds_dispatch("locate", &lwin, CIT_COMMAND));
+	assert_string_equal("Nothing to repeat", ui_sb_last());
+}
+
 static void
 strings_list_is(const strlist_t expected, const strlist_t actual)
 {
diff --git a/tests/commands/regedit.c b/tests/commands/regedit.c
new file mode 100644
index 0000000..90a31a2
--- /dev/null
+++ b/tests/commands/regedit.c
@@ -0,0 +1,106 @@
+#include <stic.h>
+
+#include <test-utils.h>
+
+#include "../../src/cfg/config.h"
+#include "../../src/compat/fs_limits.h"
+#include "../../src/engine/cmds.h"
+#include "../../src/ui/statusbar.h"
+#include "../../src/ui/ui.h"
+#include "../../src/utils/path.h"
+#include "../../src/utils/str.h"
+#include "../../src/cmd_core.h"
+#include "../../src/registers.h"
+
+SETUP()
+{
+	view_setup(&lwin);
+	curr_view = &lwin;
+
+	conf_setup();
+	cmds_init();
+	regs_init();
+}
+
+TEARDOWN()
+{
+	view_teardown(&lwin);
+	curr_view = NULL;
+
+	conf_teardown();
+	vle_cmds_reset();
+	regs_reset();
+}
+
+TEST(regedit, IF(not_windows))
+{
+	create_executable(SANDBOX_PATH "/script");
+	make_file(SANDBOX_PATH "/script",
+			"#!/bin/sh\n"
+			"sed 's/from/to/' < \"$3\" > \"$3_out\"\n"
+			"mv \"$3_out\" \"$3\"\n");
+
+	char vi_cmd[PATH_MAX + 1];
+	make_abs_path(vi_cmd, sizeof(vi_cmd), SANDBOX_PATH, "script", NULL);
+	update_string(&cfg.vi_command, vi_cmd);
+
+	ui_sb_msg("");
+	assert_failure(cmds_dispatch("regedit abc", &lwin, CIT_COMMAND));
+	assert_string_equal("Invalid argument: abc", ui_sb_last());
+	assert_failure(cmds_dispatch("regedit _", &lwin, CIT_COMMAND));
+	assert_string_equal("Cannot modify blackhole register.", ui_sb_last());
+	assert_failure(cmds_dispatch("regedit %", &lwin, CIT_COMMAND));
+	assert_string_equal("Register with given name does not exist.", ui_sb_last());
+
+	regs_append('a', "/path/before");
+	regs_append('a', "/path/from-1");
+	regs_append('a', "/path/from-2");
+	regs_append('a', "/path/to-2");
+	regs_append('a', "/path/this-was-after");
+
+	assert_success(cmds_dispatch("regedit a", &lwin, CIT_COMMAND));
+
+	/* Result should be sorted and without duplicates. */
+	const reg_t *reg = regs_find('a');
+	assert_int_equal(4, reg->nfiles);
+	assert_string_equal("/path/before", reg->files[0]);
+	assert_string_equal("/path/this-was-after", reg->files[1]);
+	assert_string_equal("/path/to-1", reg->files[2]);
+	assert_string_equal("/path/to-2", reg->files[3]);
+
+	remove_file(SANDBOX_PATH "/script");
+}
+
+TEST(regedit_normalizes_paths, IF(not_windows))
+{
+	create_executable(SANDBOX_PATH "/script");
+	make_file(SANDBOX_PATH "/script",
+			"#!/bin/sh\n"
+			"sed 's/from/to/' < \"$3\" > \"$3_out\"\n"
+			"mv \"$3_out\" \"$3\"\n");
+
+	char vi_cmd[PATH_MAX + 1];
+	make_abs_path(vi_cmd, sizeof(vi_cmd), SANDBOX_PATH, "script", NULL);
+	update_string(&cfg.vi_command, vi_cmd);
+
+	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), SANDBOX_PATH, "", NULL);
+
+	regs_append(DEFAULT_REG_NAME, "/abs/path/from-1");
+	regs_append(DEFAULT_REG_NAME, "from-2");
+
+	assert_success(cmds_dispatch("regedit", &lwin, CIT_COMMAND));
+
+	/* Result should contain only absolute paths. */
+	const reg_t *reg = regs_find(DEFAULT_REG_NAME);
+	assert_int_equal(2, reg->nfiles);
+	int rel_idx = (ends_with(reg->files[0], "/to-2") ? 0 : 1);
+	assert_string_equal("/abs/path/to-1", reg->files[1 - rel_idx]);
+	assert_string_ends_with("/to-2", reg->files[rel_idx]);
+	assert_true(is_path_absolute(reg->files[0]));
+	assert_true(is_path_absolute(reg->files[1]));
+
+	remove_file(SANDBOX_PATH "/script");
+}
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/commands/restart.c b/tests/commands/restart.c
index 063f4db..6e3efd7 100644
--- a/tests/commands/restart.c
+++ b/tests/commands/restart.c
@@ -30,7 +30,7 @@ SETUP()
 	columns_setup_column(SK_BY_NAME);
 	columns_setup_column(SK_BY_SIZE);
 
-	init_commands();
+	cmds_init();
 	opt_handlers_setup();
 }
 
@@ -39,10 +39,6 @@ TEARDOWN()
 	tabs_only(&lwin);
 	tabs_only(&rwin);
 
-	columns_free(lwin.columns);
-	lwin.columns = NULL;
-	columns_free(rwin.columns);
-	rwin.columns = NULL;
 	columns_teardown();
 
 	opt_handlers_teardown();
@@ -61,16 +57,16 @@ TEST(history_survives_in_tabs_on_restart_without_persistance)
 
 	setup_tabs();
 
-	assert_success(exec_commands("restart", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("restart", &lwin, CIT_COMMAND));
 
 	check_second_tab();
-	assert_success(exec_commands("tabprev", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabprev", &lwin, CIT_COMMAND));
 	check_first_tab();
 }
 
 TEST(restart_checks_its_parameter)
 {
-	assert_failure(exec_commands("restart wrong", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("restart wrong", &lwin, CIT_COMMAND));
 }
 
 TEST(history_survives_in_tabs_on_restart_with_persistance)
@@ -81,10 +77,10 @@ TEST(history_survives_in_tabs_on_restart_with_persistance)
 
 	setup_tabs();
 
-	assert_success(exec_commands("write | restart", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("write | restart", &lwin, CIT_COMMAND));
 
 	check_second_tab();
-	assert_success(exec_commands("tabprev", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabprev", &lwin, CIT_COMMAND));
 	check_first_tab();
 
 	remove_file(SANDBOX_PATH "/vifminfo.json");
@@ -97,7 +93,7 @@ TEST(partial_restart_preserves_tabs)
 	cfg.vifm_info = VINFO_DHISTORY | VINFO_TABS;
 
 	setup_tabs();
-	assert_success(exec_commands("restart", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("restart", &lwin, CIT_COMMAND));
 	assert_int_equal(2, tabs_count(&lwin));
 }
 
@@ -108,7 +104,7 @@ TEST(full_restart_drops_tabs)
 	cfg.vifm_info = VINFO_DHISTORY | VINFO_TABS;
 
 	setup_tabs();
-	assert_success(exec_commands("restart full", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("restart full", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_count(&lwin));
 }
 
@@ -122,7 +118,7 @@ setup_tabs(void)
 	flist_hist_setup(&rwin, "/t1rdir2", "t1rfile2", 2, 1);
 	flist_hist_setup(&rwin, "/path", "", 3, 1);
 
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
 
 	flist_hist_setup(&lwin, "/t2ldir1", "t2lfile1", 1, 1);
 	flist_hist_setup(&lwin, "/t2ldir2", "t2lfile2", 2, 1);
diff --git a/tests/commands/scope.c b/tests/commands/scope.c
index 456cd2e..d7993d4 100644
--- a/tests/commands/scope.c
+++ b/tests/commands/scope.c
@@ -28,8 +28,8 @@ SETUP_ONCE()
 
 SETUP()
 {
-	init_modes();
-	modcline_enter(CLS_COMMAND, "", NULL);
+	modes_init();
+	modcline_enter(CLS_COMMAND, "");
 }
 
 TEARDOWN()
diff --git a/tests/commands/selection.c b/tests/commands/selection.c
index 93c2b8d..8edb405 100644
--- a/tests/commands/selection.c
+++ b/tests/commands/selection.c
@@ -35,7 +35,7 @@ SETUP()
 
 	stats_update_shell_type(cfg.shell);
 
-	init_commands();
+	cmds_init();
 
 	cfg_resize_histories(10);
 
@@ -66,19 +66,19 @@ TEARDOWN()
 
 TEST(select_fails_for_wrong_pattern)
 {
-	assert_failure(exec_commands("select /**/", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("select /**/", &lwin, CIT_COMMAND));
 }
 
 TEST(select_fails_for_pattern_and_range)
 {
-	assert_failure(exec_commands("1,$select *.c", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("1,$select *.c", &lwin, CIT_COMMAND));
 }
 
 TEST(select_selects_matching_files)
 {
 	add_some_files_to_view(&lwin);
 
-	assert_success(exec_commands("select *.c", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select *.c", &lwin, CIT_COMMAND));
 
 	assert_int_equal(2, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
@@ -92,7 +92,7 @@ TEST(select_appends_matching_files_to_selection)
 	lwin.dir_entry[1].selected = 1;
 	lwin.selected_files = 2;
 
-	assert_success(exec_commands("select *.c", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select *.c", &lwin, CIT_COMMAND));
 
 	assert_int_equal(3, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
@@ -107,7 +107,7 @@ TEST(select_bang_unselects_nonmatching_files)
 	lwin.dir_entry[1].selected = 1;
 	lwin.selected_files = 2;
 
-	assert_success(exec_commands("select! *.c", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select! *.c", &lwin, CIT_COMMAND));
 
 	assert_int_equal(2, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
@@ -123,19 +123,19 @@ TEST(select_noargs_selects_current_file)
 	lwin.selected_files = 2;
 	lwin.list_pos = 2;
 
-	assert_success(exec_commands("select", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select", &lwin, CIT_COMMAND));
 	assert_int_equal(3, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_true(lwin.dir_entry[1].selected);
 	assert_true(lwin.dir_entry[2].selected);
 
-	assert_success(exec_commands("select", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select", &lwin, CIT_COMMAND));
 	assert_int_equal(3, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_true(lwin.dir_entry[1].selected);
 	assert_true(lwin.dir_entry[2].selected);
 
-	assert_success(exec_commands("select!", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select!", &lwin, CIT_COMMAND));
 	assert_int_equal(1, lwin.selected_files);
 	assert_false(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
@@ -148,13 +148,13 @@ TEST(select_can_select_range)
 	lwin.dir_entry[0].selected = 1;
 	lwin.selected_files = 1;
 
-	assert_success(exec_commands("2,$select", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("2,$select", &lwin, CIT_COMMAND));
 	assert_int_equal(3, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_true(lwin.dir_entry[1].selected);
 	assert_true(lwin.dir_entry[2].selected);
 
-	assert_success(exec_commands("2,$select!", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("2,$select!", &lwin, CIT_COMMAND));
 	assert_int_equal(2, lwin.selected_files);
 	assert_false(lwin.dir_entry[0].selected);
 	assert_true(lwin.dir_entry[1].selected);
@@ -163,12 +163,12 @@ TEST(select_can_select_range)
 
 TEST(unselect_fails_for_wrong_pattern)
 {
-	assert_failure(exec_commands("unselect /**/", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("unselect /**/", &lwin, CIT_COMMAND));
 }
 
 TEST(unselect_fails_for_pattern_and_range)
 {
-	assert_failure(exec_commands("1,$unselect *.c", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("1,$unselect *.c", &lwin, CIT_COMMAND));
 }
 
 TEST(unselect_unselects_matching_files)
@@ -178,7 +178,7 @@ TEST(unselect_unselects_matching_files)
 	lwin.dir_entry[1].selected = 1;
 	lwin.selected_files = 2;
 
-	assert_success(exec_commands("unselect *.c", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("unselect *.c", &lwin, CIT_COMMAND));
 
 	assert_int_equal(1, lwin.selected_files);
 	assert_false(lwin.dir_entry[0].selected);
@@ -194,14 +194,14 @@ TEST(unselect_noargs_unselects_current_file)
 	lwin.selected_files = 2;
 
 	lwin.list_pos = 2;
-	assert_success(exec_commands("unselect", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("unselect", &lwin, CIT_COMMAND));
 	assert_int_equal(2, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_true(lwin.dir_entry[1].selected);
 	assert_false(lwin.dir_entry[2].selected);
 
 	lwin.list_pos = 1;
-	assert_success(exec_commands("unselect", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("unselect", &lwin, CIT_COMMAND));
 	assert_int_equal(1, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
@@ -216,7 +216,7 @@ TEST(unselect_can_unselect_range)
 	lwin.dir_entry[2].selected = 1;
 	lwin.selected_files = 3;
 
-	assert_success(exec_commands("2,$unselect", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("2,$unselect", &lwin, CIT_COMMAND));
 	assert_int_equal(1, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
@@ -227,8 +227,8 @@ TEST(select_and_unselect_and_pattern_ambiguity)
 {
 	add_some_files_to_view(&lwin);
 
-	assert_success(exec_commands("select !{*.c}", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("unselect !/\\.c/", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select !{*.c}", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("unselect !/\\.c/", &lwin, CIT_COMMAND));
 	assert_int_equal(1, lwin.selected_files);
 	assert_false(lwin.dir_entry[0].selected);
 	assert_true(lwin.dir_entry[1].selected);
@@ -242,21 +242,21 @@ TEST(select_and_unselect_use_last_pattern)
 	cfg_resize_histories(5);
 
 	hists_search_save(".*\\.C");
-	assert_success(exec_commands("select! //I", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select! //I", &lwin, CIT_COMMAND));
 	assert_int_equal(0, lwin.selected_files);
 	assert_false(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
 	assert_false(lwin.dir_entry[2].selected);
 
 	hists_search_save(".*\\.c$");
-	assert_success(exec_commands("select //", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select //", &lwin, CIT_COMMAND));
 	assert_int_equal(2, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
 	assert_true(lwin.dir_entry[2].selected);
 
 	hists_search_save("a.c");
-	assert_success(exec_commands("unselect ////", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("unselect ////", &lwin, CIT_COMMAND));
 	assert_int_equal(1, lwin.selected_files);
 	assert_false(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
@@ -269,19 +269,19 @@ TEST(select_and_unselect_accept_external_command)
 {
 	add_some_files_to_view(&lwin);
 
-	assert_success(exec_commands("select !echo a.c", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select !echo a.c", &lwin, CIT_COMMAND));
 	assert_int_equal(1, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
 	assert_false(lwin.dir_entry[2].selected);
 
-	assert_success(exec_commands("select!!echo c.c", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select!!echo c.c", &lwin, CIT_COMMAND));
 	assert_int_equal(1, lwin.selected_files);
 	assert_false(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
 	assert_true(lwin.dir_entry[2].selected);
 
-	assert_success(exec_commands("unselect !echo c.c", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("unselect !echo c.c", &lwin, CIT_COMMAND));
 	assert_int_equal(0, lwin.selected_files);
 	assert_false(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
@@ -292,7 +292,7 @@ TEST(select_expands_macros_in_external_command)
 {
 	add_some_files_to_view(&lwin);
 
-	assert_success(exec_commands("select !echo %c", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select !echo %c", &lwin, CIT_COMMAND));
 	assert_int_equal(1, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
@@ -315,25 +315,25 @@ TEST(select_directory_supplied_by_external_command)
 	lwin.dir_entry[1].type = FT_REG;
 	lwin.selected_files = 0;
 
-	assert_success(exec_commands("select! !echo selection", &lwin,
+	assert_success(cmds_dispatch("select! !echo selection", &lwin,
 				CIT_COMMAND));
 	assert_int_equal(1, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
 
-	assert_success(exec_commands("select! !echo selection/", &lwin,
+	assert_success(cmds_dispatch("select! !echo selection/", &lwin,
 				CIT_COMMAND));
 	assert_int_equal(1, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
 
-	assert_success(exec_commands("select! !echo selection//////", &lwin,
+	assert_success(cmds_dispatch("select! !echo selection//////", &lwin,
 				CIT_COMMAND));
 	assert_int_equal(1, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
 
-	assert_success(exec_commands("select! !echo a.c/", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select! !echo a.c/", &lwin, CIT_COMMAND));
 	assert_int_equal(0, lwin.selected_files);
 	assert_false(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
@@ -362,7 +362,7 @@ TEST(select_and_unselect_consider_trailing_slash)
 	lwin.selected_files = 0;
 
 	/* Select only directories. */
-	assert_success(exec_commands("select */", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select */", &lwin, CIT_COMMAND));
 	assert_int_equal(2, lwin.selected_files);
 	assert_false(lwin.dir_entry[0].selected);
 	assert_true(lwin.dir_entry[1].selected);
@@ -370,7 +370,7 @@ TEST(select_and_unselect_consider_trailing_slash)
 	assert_true(lwin.dir_entry[3].selected);
 
 	/* Select both file and directory. */
-	assert_success(exec_commands("select! a", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select! a", &lwin, CIT_COMMAND));
 	assert_int_equal(2, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_true(lwin.dir_entry[1].selected);
@@ -378,7 +378,7 @@ TEST(select_and_unselect_consider_trailing_slash)
 	assert_false(lwin.dir_entry[3].selected);
 
 	/* Select only files inside given directory. */
-	assert_success(exec_commands("select! {{*/a/**}}", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select! {{*/a/**}}", &lwin, CIT_COMMAND));
 	assert_int_equal(0, lwin.selected_files);
 	assert_false(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
@@ -386,7 +386,7 @@ TEST(select_and_unselect_consider_trailing_slash)
 	assert_false(lwin.dir_entry[3].selected);
 
 	/* Select directories and files. */
-	assert_success(exec_commands("select! {{*/a}}", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select! {{*/a}}", &lwin, CIT_COMMAND));
 	assert_int_equal(2, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_true(lwin.dir_entry[1].selected);
@@ -394,7 +394,7 @@ TEST(select_and_unselect_consider_trailing_slash)
 	assert_false(lwin.dir_entry[3].selected);
 
 	/* Select only directories. */
-	assert_success(exec_commands("select! {{*/a/}}", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select! {{*/a/}}", &lwin, CIT_COMMAND));
 	assert_int_equal(1, lwin.selected_files);
 	assert_false(lwin.dir_entry[0].selected);
 	assert_true(lwin.dir_entry[1].selected);
@@ -419,7 +419,7 @@ TEST(symlinks_are_not_resolved_in_cwd, IF(not_windows))
 	lwin.selected_files = 0;
 
 	/* Select only directories. */
-	assert_success(exec_commands("select !echo a", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select !echo a", &lwin, CIT_COMMAND));
 	assert_int_equal(1, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 
@@ -430,13 +430,13 @@ TEST(select_and_unselect_can_take_location_list_as_input)
 {
 	add_some_files_to_view(&lwin);
 
-	assert_success(exec_commands("select !echo a.c:here", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select !echo a.c:here", &lwin, CIT_COMMAND));
 	assert_int_equal(1, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
 	assert_false(lwin.dir_entry[2].selected);
 
-	assert_success(exec_commands("unselect !echo a.c:here", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("unselect !echo a.c:here", &lwin, CIT_COMMAND));
 	assert_int_equal(0, lwin.selected_files);
 	assert_false(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
@@ -447,13 +447,13 @@ TEST(input_redirection, IF(have_cat))
 {
 	add_some_files_to_view(&lwin);
 
-	assert_success(exec_commands("select *", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select *", &lwin, CIT_COMMAND));
 	assert_int_equal(3, lwin.selected_files);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_true(lwin.dir_entry[1].selected);
 	assert_true(lwin.dir_entry[2].selected);
 
-	assert_success(exec_commands("unselect !cat %Pl", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("unselect !cat %Pl", &lwin, CIT_COMMAND));
 	assert_int_equal(0, lwin.selected_files);
 	assert_false(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
@@ -462,8 +462,8 @@ TEST(input_redirection, IF(have_cat))
 
 TEST(pipe_in_pattern_does_not_separate_commands)
 {
-	assert_success(exec_commands("select /first|second/", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("unselect /first|second/", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("select /first|second/", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("unselect /first|second/", &lwin, CIT_COMMAND));
 }
 
 static void
diff --git a/tests/commands/sessions.c b/tests/commands/sessions.c
index 3318954..6cdff83 100644
--- a/tests/commands/sessions.c
+++ b/tests/commands/sessions.c
@@ -41,13 +41,13 @@ SETUP()
 	columns_setup_column(SK_BY_NAME);
 	columns_setup_column(SK_BY_SIZE);
 
-	init_commands();
+	cmds_init();
 	opt_handlers_setup();
 }
 
 TEARDOWN()
 {
-	(void)exec_commands("session", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("session", &lwin, CIT_COMMAND);
 
 	cfg.config_dir[0] = '\0';
 	cfg.session_options = 0;
@@ -58,49 +58,45 @@ TEARDOWN()
 	view_teardown(&lwin);
 	view_teardown(&rwin);
 
-	columns_free(lwin.columns);
-	lwin.columns = NULL;
-	columns_free(rwin.columns);
-	rwin.columns = NULL;
 	columns_teardown();
 }
 
 TEST(can_create_a_session)
 {
 	ui_sb_msg("");
-	assert_failure(exec_commands("session sess", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session sess", &lwin, CIT_COMMAND));
 	assert_string_equal("Switched to a new session: sess", ui_sb_last());
 }
 
 TEST(query_no_session)
 {
 	ui_sb_msg("");
-	assert_failure(exec_commands("session?", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session?", &lwin, CIT_COMMAND));
 	assert_string_equal("No active session", ui_sb_last());
 }
 
 TEST(query_current_session)
 {
-	assert_failure(exec_commands("session sess", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session sess", &lwin, CIT_COMMAND));
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("session?", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session?", &lwin, CIT_COMMAND));
 	assert_string_equal("Active session: sess", ui_sb_last());
 }
 
 TEST(detach_no_session)
 {
 	ui_sb_msg("");
-	assert_failure(exec_commands("session", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session", &lwin, CIT_COMMAND));
 	assert_string_equal("No active session", ui_sb_last());
 }
 
 TEST(detach_current_session)
 {
-	assert_failure(exec_commands("session sess", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session sess", &lwin, CIT_COMMAND));
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("session", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session", &lwin, CIT_COMMAND));
 	assert_string_equal("Detached from session without saving: sess",
 			ui_sb_last());
 }
@@ -108,16 +104,16 @@ TEST(detach_current_session)
 TEST(reject_name_with_slash)
 {
 	ui_sb_msg("");
-	assert_failure(exec_commands("session a/b", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session a/b", &lwin, CIT_COMMAND));
 	assert_string_equal("Session name can't include path separators",
 			ui_sb_last());
 }
 
 TEST(reject_to_restart_current_session)
 {
-	assert_failure(exec_commands("session sess", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session sess", &lwin, CIT_COMMAND));
 	ui_sb_msg("");
-	assert_failure(exec_commands("session sess", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session sess", &lwin, CIT_COMMAND));
 	assert_string_equal("Already active session: sess", ui_sb_last());
 }
 
@@ -126,8 +122,8 @@ TEST(store_previous_session_before_switching)
 	make_abs_path(cfg.config_dir, sizeof(cfg.config_dir), SANDBOX_PATH, "", NULL);
 	cfg.session_options = VINFO_CHISTORY;
 
-	assert_failure(exec_commands("session first", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("session second", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session first", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session second", &lwin, CIT_COMMAND));
 
 	remove_file(SANDBOX_PATH "/sessions/first.json");
 	remove_dir(SANDBOX_PATH "/sessions");
@@ -139,8 +135,8 @@ TEST(session_is_stored_with_general_state)
 	make_abs_path(cfg.config_dir, sizeof(cfg.config_dir), SANDBOX_PATH, "", NULL);
 	cfg.session_options = VINFO_CHISTORY;
 
-	assert_failure(exec_commands("session session", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("write", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session session", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("write", &lwin, CIT_COMMAND));
 
 	remove_file(SANDBOX_PATH "/sessions/session.json");
 	remove_dir(SANDBOX_PATH "/sessions");
@@ -155,14 +151,14 @@ TEST(can_load_a_session)
 	histories_init(10);
 	hist_add(&curr_stats.cmd_hist, "command1", 1);
 
-	assert_failure(exec_commands("session sess", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("session other", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("session", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session sess", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session other", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session", &lwin, CIT_COMMAND));
 
 	histories_init(10);
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("session sess", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session sess", &lwin, CIT_COMMAND));
 	assert_string_equal("Loaded session: sess", ui_sb_last());
 
 	assert_int_equal(1, curr_stats.cmd_hist.size);
@@ -179,7 +175,7 @@ TEST(can_delete_a_session)
 	make_abs_path(cfg.config_dir, sizeof(cfg.config_dir), SANDBOX_PATH, "", NULL);
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("delsession sess", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("delsession sess", &lwin, CIT_COMMAND));
 	assert_string_equal("No stored sessions with such name: sess", ui_sb_last());
 
 	create_dir(SANDBOX_PATH "/sessions");
@@ -190,12 +186,12 @@ TEST(can_delete_a_session)
 	{
 		assert_success(chmod(SANDBOX_PATH "/sessions", 0555));
 		ui_sb_msg("");
-		assert_failure(exec_commands("delsession session", &lwin, CIT_COMMAND));
+		assert_failure(cmds_dispatch("delsession session", &lwin, CIT_COMMAND));
 		assert_string_equal("Failed to delete a session: session", ui_sb_last());
 		assert_success(chmod(SANDBOX_PATH "/sessions", 0777));
 	}
 
-	assert_success(exec_commands("delsession session", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("delsession session", &lwin, CIT_COMMAND));
 
 	no_remove_file(SANDBOX_PATH "/sessions/session.json");
 	remove_dir(SANDBOX_PATH "/sessions/not-a-session.json");
@@ -213,7 +209,7 @@ TEST(can_fail_to_switch_and_still_be_in_a_session)
 	make_file(SANDBOX_PATH "/vifmrc", "session bla");
 
 	ui_sb_msg("");
-	assert_failure(exec_commands("session empty", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session empty", &lwin, CIT_COMMAND));
 	assert_string_equal("Session switching has failed, active session: bla",
 			ui_sb_last());
 	char *value = var_to_str(getvar("v:session"));
@@ -238,21 +234,21 @@ TEST(load_previous_session)
 	/* No previous session. */
 	update_string(&curr_stats.last_session, NULL);
 	ui_sb_msg("");
-	assert_failure(exec_commands("session -", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session -", &lwin, CIT_COMMAND));
 	assert_string_equal("No previous session", ui_sb_last());
 
 	/* Detached session. */
-	assert_failure(exec_commands("session tmp", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("session", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session tmp", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session", &lwin, CIT_COMMAND));
 	ui_sb_msg("");
-	assert_failure(exec_commands("session -", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session -", &lwin, CIT_COMMAND));
 	assert_string_equal("Previous session doesn't exist", ui_sb_last());
 
 	/* Previous session. */
-	assert_failure(exec_commands("session first", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("session second", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session first", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session second", &lwin, CIT_COMMAND));
 	ui_sb_msg("");
-	assert_failure(exec_commands("session -", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session -", &lwin, CIT_COMMAND));
 	assert_string_equal("Loaded session: first", ui_sb_last());
 
 	remove_file(SANDBOX_PATH "/sessions/first.json");
@@ -272,7 +268,7 @@ TEST(vsession_is_empty_initially)
 
 TEST(vsession_is_set)
 {
-	assert_failure(exec_commands("session sess", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session sess", &lwin, CIT_COMMAND));
 	char *value = var_to_str(getvar("v:session"));
 	assert_string_equal("sess", value);
 	free(value);
@@ -280,8 +276,8 @@ TEST(vsession_is_set)
 
 TEST(vsession_is_empty_after_detaching)
 {
-	assert_failure(exec_commands("session sess", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("session", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session sess", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session", &lwin, CIT_COMMAND));
 	char *value = var_to_str(getvar("v:session"));
 	assert_string_equal("", value);
 	free(value);
@@ -295,9 +291,9 @@ TEST(vsession_is_emptied_on_failure_to_load_a_session)
 	create_dir(SANDBOX_PATH "/sessions");
 	create_file(SANDBOX_PATH "/sessions/empty.json");
 
-	assert_failure(exec_commands("session sess", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session sess", &lwin, CIT_COMMAND));
 	ui_sb_msg("");
-	assert_failure(exec_commands("session empty", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session empty", &lwin, CIT_COMMAND));
 	assert_string_equal("Session switching has failed, no active session",
 			ui_sb_last());
 	char *value = var_to_str(getvar("v:session"));
@@ -323,12 +319,12 @@ TEST(autocmd_is_called_for_all_global_tabs)
 
 	create_dir(SANDBOX_PATH "/sessions");
 
-	assert_failure(exec_commands("session sess", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("write", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("session", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("session sess", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("tabnext", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session sess", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("write", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session sess", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnext", &lwin, CIT_COMMAND));
 
 	int i;
 	tab_info_t tab_info;
@@ -363,12 +359,12 @@ TEST(autocmd_is_called_for_all_pane_tabs)
 
 	create_dir(SANDBOX_PATH "/sessions");
 
-	assert_failure(exec_commands("session sess", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("write", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("session", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("session sess", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("tabnext", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session sess", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("write", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("session sess", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnext", &lwin, CIT_COMMAND));
 
 	int i;
 	tab_info_t tab_info;
diff --git a/tests/commands/set.c b/tests/commands/set.c
new file mode 100644
index 0000000..1f7f2ae
--- /dev/null
+++ b/tests/commands/set.c
@@ -0,0 +1,116 @@
+#include <stic.h>
+
+#include <test-utils.h>
+
+#include "../../src/cfg/config.h"
+#include "../../src/ui/column_view.h"
+#include "../../src/ui/ui.h"
+#include "../../src/cmd_core.h"
+#include "../../src/opt_handlers.h"
+
+static void print_func(const char buf[], size_t offset, AlignType align,
+		const char full_column[], const format_info_t *info);
+
+SETUP()
+{
+	curr_view = &lwin;
+	other_view = &rwin;
+
+	conf_setup();
+	view_setup(&lwin);
+	view_setup(&rwin);
+
+	lwin.num_width_g = lwin.num_width = 4;
+	lwin.columns = columns_create();
+	rwin.num_width_g = rwin.num_width = 4;
+	rwin.columns = columns_create();
+
+	cmds_init();
+	opt_handlers_setup();
+
+	/* Name+size matches default column view setting ("-{name},{}"). */
+	columns_setup_column(SK_BY_NAME);
+	columns_setup_column(SK_BY_SIZE);
+	columns_set_line_print_func(&print_func);
+}
+
+TEARDOWN()
+{
+	curr_view = NULL;
+	other_view = NULL;
+
+	conf_teardown();
+	view_teardown(&lwin);
+	view_teardown(&rwin);
+
+	vle_cmds_reset();
+	opt_handlers_teardown();
+}
+
+TEST(set_local_sets_local_value)
+{
+	assert_success(cmds_dispatch("setlocal numberwidth=2", &lwin, CIT_COMMAND));
+	assert_int_equal(4, lwin.num_width_g);
+	assert_int_equal(2, lwin.num_width);
+}
+
+TEST(set_global_sets_global_value)
+{
+	assert_success(cmds_dispatch("setglobal numberwidth=2", &lwin, CIT_COMMAND));
+	assert_int_equal(2, lwin.num_width_g);
+	assert_int_equal(4, lwin.num_width);
+}
+
+TEST(set_sets_local_and_global_values)
+{
+	assert_success(cmds_dispatch("set numberwidth=2", &lwin, CIT_COMMAND));
+	assert_int_equal(2, lwin.num_width_g);
+	assert_int_equal(2, lwin.num_width);
+}
+
+TEST(fails_to_set_sort_group_with_wrong_regexp)
+{
+	assert_failure(cmds_dispatch("set sortgroups=*", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set sortgroups=.*,*", &lwin, CIT_COMMAND));
+}
+
+TEST(global_local_always_updates_two_views)
+{
+	lwin.ls_view_g = lwin.ls_view = 1;
+	rwin.ls_view_g = rwin.ls_view = 0;
+	lwin.hide_dot_g = lwin.hide_dot = 0;
+	rwin.hide_dot_g = rwin.hide_dot = 1;
+
+	load_view_options(curr_view);
+
+	curr_stats.global_local_settings = 1;
+	assert_success(cmds_dispatch("set nodotfiles lsview", &lwin, CIT_COMMAND));
+	assert_true(lwin.ls_view_g);
+	assert_true(lwin.ls_view);
+	assert_true(rwin.ls_view_g);
+	assert_true(rwin.ls_view);
+	assert_true(lwin.hide_dot_g);
+	assert_true(lwin.hide_dot);
+	assert_true(rwin.hide_dot_g);
+	assert_true(rwin.hide_dot);
+	curr_stats.global_local_settings = 0;
+}
+
+TEST(global_local_updates_regular_options_only_once)
+{
+	cfg.tab_stop = 0;
+
+	curr_stats.global_local_settings = 1;
+	assert_success(cmds_dispatch("set tabstop+=10", &lwin, CIT_COMMAND));
+	assert_int_equal(10, cfg.tab_stop);
+	curr_stats.global_local_settings = 0;
+}
+
+static void
+print_func(const char buf[], size_t offset, AlignType align,
+		const char full_column[], const format_info_t *info)
+{
+}
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/commands/sibl.c b/tests/commands/sibl.c
index 46d9ffe..9eb4200 100644
--- a/tests/commands/sibl.c
+++ b/tests/commands/sibl.c
@@ -26,7 +26,7 @@ SETUP()
 {
 	view_setup(&lwin);
 
-	init_commands();
+	cmds_init();
 
 	lwin.sort_g[0] = SK_BY_NAME;
 	memset(&lwin.sort_g[1], SK_NONE, sizeof(lwin.sort_g) - 1);
@@ -52,7 +52,7 @@ TEST(sibl_do_nothing_in_cv)
 	flist_custom_add(&lwin, path);
 	assert_true(flist_custom_finish(&lwin, CV_REGULAR, 0) == 0);
 
-	assert_success(exec_commands("siblnext", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("siblnext", &lwin, CIT_COMMAND));
 	assert_true(flist_custom_active(&lwin));
 }
 
@@ -63,19 +63,19 @@ TEST(sibl_navigate_correctly)
 	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), TEST_DATA_PATH, "read",
 			cwd);
 
-	assert_success(exec_commands("siblnext", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("siblnext", &lwin, CIT_COMMAND));
 	make_abs_path(path, sizeof(path), TEST_DATA_PATH, "rename", cwd);
 	assert_true(paths_are_same(lwin.curr_dir, path));
 
-	assert_success(exec_commands("siblprev", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("siblprev", &lwin, CIT_COMMAND));
 	make_abs_path(path, sizeof(path), TEST_DATA_PATH, "read", cwd);
 	assert_true(paths_are_same(lwin.curr_dir, path));
 
-	assert_success(exec_commands("2siblnext", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("2siblnext", &lwin, CIT_COMMAND));
 	make_abs_path(path, sizeof(path), TEST_DATA_PATH, "scripts", cwd);
 	assert_true(paths_are_same(lwin.curr_dir, path));
 
-	assert_success(exec_commands("3siblprev", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("3siblprev", &lwin, CIT_COMMAND));
 	make_abs_path(path, sizeof(path), TEST_DATA_PATH, "quotes-in-names", cwd);
 	assert_true(paths_are_same(lwin.curr_dir, path));
 }
@@ -85,13 +85,13 @@ TEST(sibl_does_not_wrap_by_default)
 	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), TEST_DATA_PATH,
 			"various-sizes", cwd);
 
-	assert_success(exec_commands("siblnext", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("siblnext", &lwin, CIT_COMMAND));
 	assert_true(paths_are_same(lwin.curr_dir, TEST_DATA_PATH "/various-sizes"));
 
 	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), TEST_DATA_PATH,
 			"color-schemes", cwd);
 
-	assert_success(exec_commands("siblprev", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("siblprev", &lwin, CIT_COMMAND));
 	assert_true(paths_are_same(lwin.curr_dir, TEST_DATA_PATH "/color-schemes"));
 }
 
@@ -102,11 +102,11 @@ TEST(sibl_wrap)
 	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), TEST_DATA_PATH,
 			"various-sizes", cwd);
 
-	exec_commands("siblnext!", &lwin, CIT_COMMAND);
+	cmds_dispatch("siblnext!", &lwin, CIT_COMMAND);
 	make_abs_path(path, sizeof(path), TEST_DATA_PATH, "color-schemes", cwd);
 	assert_true(paths_are_same(lwin.curr_dir, path));
 
-	exec_commands("siblprev!", &lwin, CIT_COMMAND);
+	cmds_dispatch("siblprev!", &lwin, CIT_COMMAND);
 	make_abs_path(path, sizeof(path), TEST_DATA_PATH, "various-sizes", cwd);
 	assert_true(paths_are_same(lwin.curr_dir, path));
 }
@@ -118,11 +118,11 @@ TEST(sibl_handles_errors_without_failures)
 			cwd);
 
 	strcpy(lwin.curr_dir, "not/a/valid/path");
-	exec_commands("siblnext!", &lwin, CIT_COMMAND);
+	cmds_dispatch("siblnext!", &lwin, CIT_COMMAND);
 	assert_string_equal(lwin.curr_dir, "not/a/valid/path");
 
 	strcpy(lwin.curr_dir, path);
-	exec_commands("siblprev", &lwin, CIT_COMMAND);
+	cmds_dispatch("siblprev", &lwin, CIT_COMMAND);
 	assert_true(paths_are_same(lwin.curr_dir, path));
 }
 
@@ -139,7 +139,7 @@ TEST(sibl_skips_files_and_can_work_without_sorting)
 	lwin.sort_g[0] = SK_NONE;
 
 	strcpy(lwin.curr_dir, path);
-	exec_commands("siblnext!", &lwin, CIT_COMMAND);
+	cmds_dispatch("siblnext!", &lwin, CIT_COMMAND);
 	assert_true(paths_are_same(lwin.curr_dir, path));
 
 	restore_cwd(saved_cwd);
@@ -158,7 +158,7 @@ TEST(sibl_uses_global_sort_option)
 	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), TEST_DATA_PATH, "read",
 			cwd);
 
-	assert_success(exec_commands("siblnext", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("siblnext", &lwin, CIT_COMMAND));
 	assert_true(paths_are_same(lwin.curr_dir, path));
 }
 
@@ -174,11 +174,11 @@ TEST(sibl_respects_dot_filter)
 	make_abs_path(path, sizeof(path), SANDBOX_PATH, "b", cwd);
 
 	strcpy(lwin.curr_dir, path);
-	assert_success(exec_commands("siblnext", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("siblnext", &lwin, CIT_COMMAND));
 	assert_true(paths_are_same(lwin.curr_dir, path));
 
 	strcpy(lwin.curr_dir, path);
-	assert_success(exec_commands("siblprev", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("siblprev", &lwin, CIT_COMMAND));
 	assert_true(paths_are_same(lwin.curr_dir, path));
 
 	assert_success(rmdir(SANDBOX_PATH "/.a"));
@@ -201,11 +201,11 @@ TEST(sibl_respects_name_filters)
 	make_abs_path(path, sizeof(path), SANDBOX_PATH, "b", cwd);
 
 	strcpy(lwin.curr_dir, path);
-	assert_success(exec_commands("siblnext", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("siblnext", &lwin, CIT_COMMAND));
 	assert_true(paths_are_same(lwin.curr_dir, path));
 
 	strcpy(lwin.curr_dir, path);
-	assert_success(exec_commands("siblprev", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("siblprev", &lwin, CIT_COMMAND));
 	assert_true(paths_are_same(lwin.curr_dir, path));
 
 	assert_success(rmdir(SANDBOX_PATH "/a"));
@@ -223,7 +223,7 @@ TEST(sibl_works_inside_filtered_out_directory)
 	assert_success(os_mkdir(SANDBOX_PATH "/.b", 0700));
 
 	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), SANDBOX_PATH, ".b", cwd);
-	assert_success(exec_commands("siblnext", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("siblnext", &lwin, CIT_COMMAND));
 	make_abs_path(path, sizeof(path), SANDBOX_PATH, "a", cwd);
 	assert_true(paths_are_same(lwin.curr_dir, path));
 
diff --git a/tests/commands/suite.c b/tests/commands/suite.c
index ac91be0..0d65a5d 100644
--- a/tests/commands/suite.c
+++ b/tests/commands/suite.c
@@ -25,7 +25,7 @@ SETUP_ONCE()
 	 * nothing will change the path before we try to save it. */
 	saved_cwd = save_cwd();
 
-	bg_init();
+	assert_success(bg_init());
 
 	tabs_init();
 }
diff --git a/tests/commands/sync.c b/tests/commands/sync.c
index 948e2d7..32d64f7 100644
--- a/tests/commands/sync.c
+++ b/tests/commands/sync.c
@@ -30,7 +30,7 @@ SETUP()
 	curr_view = &lwin;
 	other_view = &rwin;
 
-	init_commands();
+	cmds_init();
 
 	cfg.slow_fs_list = strdup("");
 
@@ -63,7 +63,7 @@ TEST(sync_syncs_local_filter)
 	populate_dir_list(curr_view, 0);
 	local_filter_apply(curr_view, "a");
 
-	assert_success(exec_commands("sync! location filters", curr_view,
+	assert_success(cmds_dispatch("sync! location filters", curr_view,
 				CIT_COMMAND));
 	assert_string_equal("a", other_view->local_filter.filter.raw);
 }
@@ -92,7 +92,7 @@ TEST(sync_syncs_filelist)
 	assert_true(flist_custom_finish(curr_view, CV_VERY, 0) == 0);
 	curr_view->list_pos = 3;
 
-	assert_success(exec_commands("sync! filelist cursorpos", curr_view,
+	assert_success(cmds_dispatch("sync! filelist cursorpos", curr_view,
 				CIT_COMMAND));
 
 	assert_true(flist_custom_active(other_view));
@@ -114,7 +114,7 @@ TEST(sync_removes_leafs_and_tree_data_on_converting_tree_to_cv)
 	assert_success(flist_load_tree(curr_view, SANDBOX_PATH, INT_MAX));
 	assert_int_equal(2, curr_view->list_rows);
 
-	assert_success(exec_commands("sync! filelist", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("sync! filelist", curr_view, CIT_COMMAND));
 	restore_cwd(saved_cwd);
 	saved_cwd = save_cwd();
 
@@ -147,7 +147,7 @@ TEST(sync_syncs_trees)
 	flist_custom_exclude(curr_view, 1);
 	assert_int_equal(0, curr_view->selected_files);
 
-	assert_success(exec_commands("sync! tree", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("sync! tree", curr_view, CIT_COMMAND));
 	assert_true(flist_custom_active(other_view));
 	curr_stats.load_stage = 2;
 	load_saving_pos(other_view);
@@ -155,9 +155,6 @@ TEST(sync_syncs_trees)
 
 	assert_int_equal(curr_view->list_rows, other_view->list_rows);
 
-	columns_free(other_view->columns);
-	other_view->columns = NULL;
-
 	columns_teardown();
 }
 
@@ -177,11 +174,9 @@ TEST(sync_all_does_not_turn_destination_into_tree)
 	populate_dir_list(curr_view, 0);
 	local_filter_apply(curr_view, "a");
 
-	assert_success(exec_commands("sync! all", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("sync! all", curr_view, CIT_COMMAND));
 	assert_false(other_view->custom.type == CV_TREE);
 
-	columns_free(other_view->columns);
-	other_view->columns = NULL;
 	opt_handlers_teardown();
 
 	columns_teardown();
@@ -208,12 +203,10 @@ TEST(sync_localopts_clones_local_options)
 	populate_dir_list(curr_view, 0);
 	local_filter_apply(curr_view, "a");
 
-	assert_success(exec_commands("sync! localopts", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("sync! localopts", curr_view, CIT_COMMAND));
 	assert_true(rwin.hide_dot_g);
 	assert_true(rwin.hide_dot);
 
-	columns_free(other_view->columns);
-	other_view->columns = NULL;
 	opt_handlers_teardown();
 	columns_teardown();
 }
@@ -246,7 +239,7 @@ TEST(tree_syncing_applies_properties_of_destination_view)
 	 * is this a bug?). */
 	local_filter_apply(other_view, "d");
 
-	assert_success(exec_commands("sync! tree", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("sync! tree", curr_view, CIT_COMMAND));
 	assert_int_equal(2, other_view->list_rows);
 	assert_string_equal("", other_view->local_filter.filter.raw);
 
@@ -258,9 +251,6 @@ TEST(tree_syncing_applies_properties_of_destination_view)
 	assert_int_equal(2, other_view->list_rows);
 	assert_string_equal("", other_view->local_filter.filter.raw);
 
-	columns_free(other_view->columns);
-	other_view->columns = NULL;
-
 	columns_teardown();
 }
 
@@ -292,7 +282,7 @@ TEST(symlinks_in_paths_are_not_resolved, IF(not_windows))
 	to_canonic_path(buf, "/fake-root", curr_view->curr_dir,
 			sizeof(curr_view->curr_dir));
 
-	assert_success(exec_commands("sync ../dir-link/..", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("sync ../dir-link/..", curr_view, CIT_COMMAND));
 	restore_cwd(saved_cwd);
 	saved_cwd = save_cwd();
 
@@ -304,7 +294,7 @@ TEST(symlinks_in_paths_are_not_resolved, IF(not_windows))
 
 TEST(incorrect_parameter_causes_error)
 {
-	assert_failure(exec_commands("sync! nosuchthing", curr_view, CIT_COMMAND));
+	assert_failure(cmds_dispatch("sync! nosuchthing", curr_view, CIT_COMMAND));
 }
 
 TEST(sync_syncs_custom_trees)
@@ -341,7 +331,7 @@ TEST(sync_syncs_custom_trees)
 
 	/* As custom trees. */
 
-	assert_success(exec_commands("sync! tree", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("sync! tree", curr_view, CIT_COMMAND));
 	assert_true(flist_custom_active(other_view));
 	curr_stats.load_stage = 2;
 	load_saving_pos(other_view);
@@ -352,13 +342,10 @@ TEST(sync_syncs_custom_trees)
 
 	/* As custom views. */
 
-	assert_success(exec_commands("sync! filelist", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("sync! filelist", curr_view, CIT_COMMAND));
 	assert_true(flist_custom_active(other_view));
 	assert_int_equal(CV_VERY, other_view->custom.type);
 
-	columns_free(other_view->columns);
-	other_view->columns = NULL;
-
 	opt_handlers_teardown();
 	columns_teardown();
 }
@@ -374,16 +361,14 @@ TEST(sync_all_applies_filters_in_trees)
 	make_abs_path(curr_view->curr_dir, sizeof(curr_view->curr_dir),
 			TEST_DATA_PATH, "", NULL);
 
-	assert_success(exec_commands("set cvoptions=localfilter", curr_view,
+	assert_success(cmds_dispatch("set cvoptions=localfilter", curr_view,
 				CIT_COMMAND));
-	assert_success(exec_commands("tree", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("tree", curr_view, CIT_COMMAND));
 	local_filter_apply(curr_view, "a");
-	assert_success(exec_commands("sync! all", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("sync! all", curr_view, CIT_COMMAND));
 
 	assert_string_equal("a", other_view->local_filter.filter.raw);
 
-	columns_free(other_view->columns);
-	other_view->columns = NULL;
 	columns_teardown();
 	opt_handlers_teardown();
 }
diff --git a/tests/commands/tabs.c b/tests/commands/tabs.c
index e4ba9b3..4c8b8d6 100644
--- a/tests/commands/tabs.c
+++ b/tests/commands/tabs.c
@@ -30,14 +30,14 @@ SETUP()
 	setup_grid(&rwin, 1, 1, 1);
 	other_view = &rwin;
 
-	init_modes();
+	modes_init();
 
 	opt_handlers_setup();
 
 	columns_setup_column(SK_BY_NAME);
 	columns_setup_column(SK_BY_SIZE);
 
-	init_commands();
+	cmds_init();
 }
 
 TEARDOWN()
@@ -63,7 +63,7 @@ TEST(tab_is_created_without_name)
 {
 	tab_info_t tab_info;
 
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
 	assert_int_equal(2, tabs_count(&lwin));
 
 	assert_true(tabs_get(&lwin, 1, &tab_info));
@@ -76,7 +76,7 @@ TEST(tab_is_not_created_on_wrong_path)
 	assert_non_null(get_cwd(cwd, sizeof(cwd)));
 	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), TEST_DATA_PATH, "", cwd);
 
-	(void)exec_commands("tabnew no-such-subdir", &lwin, CIT_COMMAND);
+	(void)cmds_dispatch("tabnew no-such-subdir", &lwin, CIT_COMMAND);
 	assert_int_equal(1, tabs_count(&lwin));
 
 	tab_info_t tab_info;
@@ -94,7 +94,7 @@ TEST(tab_in_path_is_created)
 
 	strcpy(lwin.curr_dir, test_data);
 
-	assert_success(exec_commands("tabnew read", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew read", &lwin, CIT_COMMAND));
 	assert_int_equal(2, tabs_count(&lwin));
 
 	tab_info_t tab_info;
@@ -115,7 +115,7 @@ TEST(tab_in_parent_is_created)
 
 	snprintf(lwin.curr_dir, sizeof(lwin.curr_dir), "%s/read", test_data);
 
-	assert_success(exec_commands("tabnew ..", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew ..", &lwin, CIT_COMMAND));
 	assert_int_equal(2, tabs_count(&lwin));
 
 	tab_info_t tab_info;
@@ -130,8 +130,8 @@ TEST(newtab_fails_in_diff_mode_for_tab_panes)
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/b");
 
 	cfg.pane_tabs = 1;
-	(void)compare_two_panes(CT_CONTENTS, LT_ALL, 1, 0);
-	assert_failure(exec_commands("tabnew", &lwin, CIT_COMMAND));
+	(void)compare_two_panes(CT_CONTENTS, LT_ALL, CF_GROUP_PATHS | CF_SHOW);
+	assert_failure(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_count(&lwin));
 }
 
@@ -139,7 +139,7 @@ TEST(tab_name_is_set)
 {
 	tab_info_t tab_info;
 
-	assert_success(exec_commands("tabname new-name", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabname new-name", &lwin, CIT_COMMAND));
 
 	assert_true(tabs_get(&lwin, 0, &tab_info));
 	assert_string_equal("new-name", tab_info.name);
@@ -149,8 +149,8 @@ TEST(tab_name_is_reset)
 {
 	tab_info_t tab_info;
 
-	assert_success(exec_commands("tabname new-name", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("tabname", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabname new-name", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabname", &lwin, CIT_COMMAND));
 
 	assert_true(tabs_get(&lwin, 0, &tab_info));
 	assert_string_equal(NULL, tab_info.name);
@@ -158,50 +158,50 @@ TEST(tab_name_is_reset)
 
 TEST(tab_is_closed)
 {
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("tabclose", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabclose", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_count(&lwin));
 }
 
 TEST(last_tab_is_not_closed)
 {
-	assert_success(exec_commands("tabclose", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabclose", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_count(&lwin));
 }
 
 TEST(quit_commands_close_tabs)
 {
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("quit", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("quit", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_count(&lwin));
 
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("wq", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("wq", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_count(&lwin));
 
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
 	(void)vle_keys_exec_timed_out(WK_Z WK_Z);
 	assert_int_equal(1, tabs_count(&lwin));
 
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
 	(void)vle_keys_exec_timed_out(WK_Z WK_Q);
 	assert_int_equal(1, tabs_count(&lwin));
 }
 
 TEST(quit_all_commands_ignore_tabs)
 {
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
 
 	vifm_tests_exited = 0;
-	assert_success(exec_commands("qall", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("qall", &lwin, CIT_COMMAND));
 	assert_true(vifm_tests_exited);
 
 	vifm_tests_exited = 0;
-	assert_success(exec_commands("wqall", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("wqall", &lwin, CIT_COMMAND));
 	assert_true(vifm_tests_exited);
 
 	vifm_tests_exited = 0;
-	assert_success(exec_commands("xall", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("xall", &lwin, CIT_COMMAND));
 	assert_true(vifm_tests_exited);
 
 	assert_int_equal(2, tabs_count(&lwin));
@@ -209,7 +209,7 @@ TEST(quit_all_commands_ignore_tabs)
 
 TEST(tabs_are_switched_with_shortcuts)
 {
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
 
 	(void)vle_keys_exec_timed_out(WK_g WK_t);
 	assert_int_equal(0, tabs_current(&lwin));
@@ -223,61 +223,61 @@ TEST(tabs_are_switched_with_shortcuts)
 
 TEST(tabs_are_switched_with_commands)
 {
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
 
 	/* Valid arguments. */
 
-	assert_success(exec_commands("tabnext", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnext", &lwin, CIT_COMMAND));
 	assert_int_equal(0, tabs_current(&lwin));
 
-	assert_success(exec_commands("tabnext", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnext", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_current(&lwin));
 
-	assert_success(exec_commands("tabnext 1", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnext 1", &lwin, CIT_COMMAND));
 	assert_int_equal(0, tabs_current(&lwin));
 
-	assert_success(exec_commands("tabnext 1", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnext 1", &lwin, CIT_COMMAND));
 	assert_int_equal(0, tabs_current(&lwin));
 
-	assert_success(exec_commands("tabnext 2", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnext 2", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_current(&lwin));
 
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
 	assert_int_equal(2, tabs_current(&lwin));
 
-	assert_success(exec_commands("tabprevious", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabprevious", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_current(&lwin));
 
-	assert_success(exec_commands("tabprevious 2", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabprevious 2", &lwin, CIT_COMMAND));
 	assert_int_equal(2, tabs_current(&lwin));
 
-	assert_success(exec_commands("tabprevious 3", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabprevious 3", &lwin, CIT_COMMAND));
 	assert_int_equal(2, tabs_current(&lwin));
 
-	assert_success(exec_commands("tabprevious 4", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabprevious 4", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_current(&lwin));
 
 	/* Invalid arguments. */
 
-	assert_failure(exec_commands("tabnext 1z", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("tabnext 1z", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_current(&lwin));
 
-	assert_failure(exec_commands("tabnext -1", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("tabnext -1", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_current(&lwin));
 
-	assert_failure(exec_commands("tabnext 4", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("tabnext 4", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_current(&lwin));
 
-	assert_failure(exec_commands("tabnext 10", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("tabnext 10", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_current(&lwin));
 
-	assert_failure(exec_commands("tabprevious 0", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("tabprevious 0", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_current(&lwin));
 
-	assert_failure(exec_commands("tabprevious -1", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("tabprevious -1", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_current(&lwin));
 
-	assert_failure(exec_commands("tabprevious -1", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("tabprevious -1", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_current(&lwin));
 }
 
@@ -285,39 +285,39 @@ TEST(tabs_are_moved)
 {
 	for (cfg.pane_tabs = 0; cfg.pane_tabs < 2; ++cfg.pane_tabs)
 	{
-		assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
-		assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
+		assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
+		assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
 
 		assert_int_equal(2, tabs_current(&lwin));
 
-		assert_success(exec_commands("tabmove 0", &lwin, CIT_COMMAND));
+		assert_success(cmds_dispatch("tabmove 0", &lwin, CIT_COMMAND));
 		assert_int_equal(0, tabs_current(&lwin));
-		assert_success(exec_commands("tabmove 1", &lwin, CIT_COMMAND));
+		assert_success(cmds_dispatch("tabmove 1", &lwin, CIT_COMMAND));
 		assert_int_equal(0, tabs_current(&lwin));
 
-		assert_success(exec_commands("tabmove 2", &lwin, CIT_COMMAND));
+		assert_success(cmds_dispatch("tabmove 2", &lwin, CIT_COMMAND));
 		assert_int_equal(1, tabs_current(&lwin));
-		assert_success(exec_commands("tabmove 2", &lwin, CIT_COMMAND));
+		assert_success(cmds_dispatch("tabmove 2", &lwin, CIT_COMMAND));
 		assert_int_equal(1, tabs_current(&lwin));
 
-		assert_success(exec_commands("tabmove 3", &lwin, CIT_COMMAND));
+		assert_success(cmds_dispatch("tabmove 3", &lwin, CIT_COMMAND));
 		assert_int_equal(2, tabs_current(&lwin));
-		assert_success(exec_commands("tabmove 3", &lwin, CIT_COMMAND));
+		assert_success(cmds_dispatch("tabmove 3", &lwin, CIT_COMMAND));
 		assert_int_equal(2, tabs_current(&lwin));
 
-		assert_success(exec_commands("tabmove 1", &lwin, CIT_COMMAND));
+		assert_success(cmds_dispatch("tabmove 1", &lwin, CIT_COMMAND));
 		assert_int_equal(1, tabs_current(&lwin));
-		assert_success(exec_commands("tabmove", &lwin, CIT_COMMAND));
+		assert_success(cmds_dispatch("tabmove", &lwin, CIT_COMMAND));
 		assert_int_equal(2, tabs_current(&lwin));
 
-		assert_success(exec_commands("tabmove 0", &lwin, CIT_COMMAND));
+		assert_success(cmds_dispatch("tabmove 0", &lwin, CIT_COMMAND));
 		assert_int_equal(0, tabs_current(&lwin));
-		assert_success(exec_commands("tabmove $", &lwin, CIT_COMMAND));
+		assert_success(cmds_dispatch("tabmove $", &lwin, CIT_COMMAND));
 		assert_int_equal(2, tabs_current(&lwin));
 
-		assert_success(exec_commands("tabmove 0", &lwin, CIT_COMMAND));
+		assert_success(cmds_dispatch("tabmove 0", &lwin, CIT_COMMAND));
 		assert_int_equal(0, tabs_current(&lwin));
-		assert_failure(exec_commands("tabmove wrong", &lwin, CIT_COMMAND));
+		assert_failure(cmds_dispatch("tabmove wrong", &lwin, CIT_COMMAND));
 		assert_int_equal(0, tabs_current(&lwin));
 
 		tabs_only(&lwin);
@@ -335,13 +335,13 @@ TEST(view_mode_is_fine_with_tabs)
 	(void)vle_keys_exec_timed_out(WK_e);
 	(void)vle_keys_exec_timed_out(WK_q);
 
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
 	assert_int_equal(2, tabs_count(&lwin));
 
 	(void)vle_keys_exec_timed_out(WK_e);
 	(void)vle_keys_exec_timed_out(WK_q);
 
-	assert_success(exec_commands("tabclose", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabclose", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_count(&lwin));
 
 	(void)vle_keys_exec_timed_out(WK_e);
@@ -359,19 +359,19 @@ TEST(left_view_mode_is_fine_with_tabs)
 	(void)vle_keys_exec_timed_out(WK_e);
 	(void)vle_keys_exec_timed_out(WK_C_i);
 
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
 	assert_int_equal(2, tabs_count(&lwin));
 
 	(void)vle_keys_exec_timed_out(WK_SPACE);
 	(void)vle_keys_exec_timed_out(WK_e);
 	(void)vle_keys_exec_timed_out(WK_C_i);
 
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
 	assert_int_equal(3, tabs_count(&lwin));
 
-	assert_success(exec_commands("q", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("q", &lwin, CIT_COMMAND));
 	assert_int_equal(2, tabs_count(&lwin));
-	assert_success(exec_commands("q", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("q", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_count(&lwin));
 
 	(void)vle_keys_exec_timed_out(WK_SPACE);
@@ -390,7 +390,7 @@ TEST(hidden_tabs_are_updated_on_cs_invalidation)
 
 	curr_stats.cs = &cfg.cs;
 
-	assert_success(exec_commands("highlight {*.vifm} cterm=bold", &lwin,
+	assert_success(cmds_dispatch("highlight {*.vifm} cterm=bold", &lwin,
 				CIT_COMMAND));
 	assert_non_null(cs_get_file_hi(curr_stats.cs, "some.vifm",
 				&lwin.dir_entry[0].hi_num));
@@ -399,7 +399,7 @@ TEST(hidden_tabs_are_updated_on_cs_invalidation)
 	assert_int_equal(0, lwin.dir_entry[0].hi_num);
 	assert_int_equal(0, rwin.dir_entry[0].hi_num);
 
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
 
 	tab_info_t tab_info;
 
@@ -410,7 +410,7 @@ TEST(hidden_tabs_are_updated_on_cs_invalidation)
 	assert_int_equal(0, tab_info.view->dir_entry[0].hi_num);
 	assert_int_equal(0, rwin.dir_entry[0].hi_num);
 
-	assert_success(exec_commands("highlight clear", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("highlight clear", &lwin, CIT_COMMAND));
 
 	assert_true(tabs_enum(&lwin, 0, &tab_info));
 	assert_int_equal(-1, tab_info.view->dir_entry[0].hi_num);
@@ -423,22 +423,22 @@ TEST(hidden_tabs_are_updated_on_cs_invalidation)
 TEST(setting_tabscope_drops_previous_tabs)
 {
 	assert_false(cfg.pane_tabs);
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
 
-	assert_success(exec_commands("set tabscope=pane", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set tabscope=pane", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_count(&lwin));
 	assert_int_equal(1, tabs_count(&rwin));
 	assert_true(cfg.pane_tabs);
 
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
 	swap_view_roles();
-	assert_success(exec_commands("tabnew", &rwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &rwin, CIT_COMMAND));
 
-	assert_success(exec_commands("set tabscope=global", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set tabscope=global", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_count(&lwin));
 	assert_false(cfg.pane_tabs);
 
-	assert_success(exec_commands("set tabscope=pane", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set tabscope=pane", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_count(&lwin));
 	assert_int_equal(1, tabs_count(&rwin));
 	assert_true(cfg.pane_tabs);
@@ -446,33 +446,33 @@ TEST(setting_tabscope_drops_previous_tabs)
 
 TEST(tabonly_leave_only_one_global_tab)
 {
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", &lwin, CIT_COMMAND));
 
-	assert_success(exec_commands("tabonly", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabonly", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_count(&lwin));
 }
 
 TEST(tabonly_leaves_only_one_pane_tab)
 {
 	cfg.pane_tabs = 1;
-	assert_success(exec_commands("tabnew", curr_view, CIT_COMMAND));
-	assert_success(exec_commands("tabnew", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", curr_view, CIT_COMMAND));
 
-	assert_success(exec_commands("tabonly", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabonly", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_count(curr_view));
 }
 
 TEST(tabonly_keeps_inactive_side_intact)
 {
 	cfg.pane_tabs = 1;
-	assert_success(exec_commands("tabnew", curr_view, CIT_COMMAND));
-	assert_success(exec_commands("tabnew", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", curr_view, CIT_COMMAND));
 	swap_view_roles();
-	assert_success(exec_commands("tabnew", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabnew", curr_view, CIT_COMMAND));
 	swap_view_roles();
 
-	assert_success(exec_commands("tabonly", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("tabonly", &lwin, CIT_COMMAND));
 	assert_int_equal(1, tabs_count(curr_view));
 	assert_int_equal(2, tabs_count(other_view));
 }
diff --git a/tests/commands/wincmd.c b/tests/commands/wincmd.c
index 947faff..e937aaa 100644
--- a/tests/commands/wincmd.c
+++ b/tests/commands/wincmd.c
@@ -14,7 +14,7 @@
 
 SETUP()
 {
-	init_modes();
+	modes_init();
 
 	view_setup(&lwin);
 	view_setup(&rwin);
@@ -24,7 +24,7 @@ SETUP()
 
 	opt_handlers_setup();
 
-	init_commands();
+	cmds_init();
 }
 
 TEARDOWN()
@@ -43,12 +43,12 @@ TEST(wincmd_can_switch_views)
 
 	curr_view = &rwin;
 	other_view = &lwin;
-	assert_success(exec_commands("wincmd h", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("wincmd h", curr_view, CIT_COMMAND));
 	assert_true(curr_view == &lwin);
 
 	curr_view = &rwin;
 	other_view = &lwin;
-	assert_success(exec_commands("execute 'wincmd h'", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("execute 'wincmd h'", curr_view, CIT_COMMAND));
 	assert_true(curr_view == &lwin);
 
 	init_builtin_functions();
@@ -56,14 +56,14 @@ TEST(wincmd_can_switch_views)
 	curr_view = &rwin;
 	other_view = &lwin;
 	assert_success(
-			exec_commands("if paneisat('left') == 0 | execute 'wincmd h' | endif",
+			cmds_dispatch("if paneisat('left') == 0 | execute 'wincmd h' | endif",
 				curr_view, CIT_COMMAND));
 	assert_true(curr_view == &lwin);
 
 	curr_view = &rwin;
 	other_view = &lwin;
 	assert_success(
-			exec_commands("if paneisat('left') == 0 "
+			cmds_dispatch("if paneisat('left') == 0 "
 			             "|    execute 'wincmd h' "
 			             "|    let $a = paneisat('left') "
 			             "|endif",
@@ -80,8 +80,8 @@ TEST(wincmd_ignores_mappings)
 {
 	curr_view = &rwin;
 	other_view = &lwin;
-	assert_success(exec_commands("nnoremap <c-w> <nop>", curr_view, CIT_COMMAND));
-	assert_success(exec_commands("wincmd H", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("nnoremap <c-w> <nop>", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("wincmd H", curr_view, CIT_COMMAND));
 	assert_true(curr_view == &lwin);
 }
 
diff --git a/tests/fileops/delete.c b/tests/fileops/delete.c
index 6aa4618..b2ec0e5 100644
--- a/tests/fileops/delete.c
+++ b/tests/fileops/delete.c
@@ -8,6 +8,7 @@
 #include <test-utils.h>
 
 #include "../../src/compat/fs_limits.h"
+#include "../../src/compat/os.h"
 #include "../../src/cfg/config.h"
 #include "../../src/ui/ui.h"
 #include "../../src/utils/fs.h"
@@ -18,7 +19,10 @@
 #include "../../src/registers.h"
 #include "../../src/trash.h"
 
+static char options_prompt_abort(const struct custom_prompt_t *details);
+
 static char *saved_cwd;
+static int prompt_invocations;
 
 SETUP()
 {
@@ -295,5 +299,40 @@ TEST(empty_directory_is_removed)
 	}
 }
 
+TEST(aborting_file_deletion_aborts_remaining_files, IF(regular_unix_user))
+{
+	create_dir("ro");
+	create_file("ro/a");
+	create_file("ro/b");
+	assert_success(os_chmod("ro", 0555));
+
+	prompt_invocations = 0;
+	fops_init(/*line_func=*/NULL, &options_prompt_abort);
+
+	flist_custom_start(&lwin, "test");
+	flist_custom_add(&lwin, "ro/a");
+	flist_custom_add(&lwin, "ro/b");
+	assert_true(flist_custom_finish(&lwin, CV_REGULAR, 0) == 0);
+	assert_int_equal(2, lwin.list_rows);
+
+	lwin.dir_entry[0].marked = 1;
+	lwin.dir_entry[1].marked = 1;
+	(void)fops_delete(&lwin, /*reg=*/'\0', /*use_trash=*/0);
+
+	assert_int_equal(1, prompt_invocations);
+
+	assert_success(os_chmod("ro", 0777));
+	remove_file("ro/a");
+	remove_file("ro/b");
+	remove_dir("ro");
+}
+
+static char
+options_prompt_abort(const struct custom_prompt_t *details)
+{
+	++prompt_invocations;
+	return 'a';
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/fileops/make_dirs.c b/tests/fileops/make_dirs.c
index ae16a46..62a79fc 100644
--- a/tests/fileops/make_dirs.c
+++ b/tests/fileops/make_dirs.c
@@ -19,8 +19,7 @@
 #include "../../src/fops_common.h"
 #include "../../src/fops_misc.h"
 
-static char choice_func(const char title[], const char message[],
-		const struct response_variant *variants);
+static char choice_func(const struct custom_prompt_t *details);
 
 static char *saved_cwd;
 static int choice_called = 0;
@@ -200,8 +199,7 @@ TEST(make_dirs_errors_are_handled)
 }
 
 static char
-choice_func(const char title[], const char message[],
-		const struct response_variant *variants)
+choice_func(const struct custom_prompt_t *details)
 {
 	++choice_called;
 	return 'a';
diff --git a/tests/fileops/put_files.c b/tests/fileops/put_files.c
index da2be05..93495b4 100644
--- a/tests/fileops/put_files.c
+++ b/tests/fileops/put_files.c
@@ -21,27 +21,17 @@
 #include "../../src/trash.h"
 
 static void line_prompt(const char prompt[], const char filename[],
-		fo_prompt_cb cb, fo_complete_cmd_func complete, int allow_ee);
+		fo_prompt_cb cb, void *cb_arg, fo_complete_cmd_func complete, int allow_ee);
 static void line_prompt_rec(const char prompt[], const char filename[],
-		fo_prompt_cb cb, fo_complete_cmd_func complete, int allow_ee);
-static char options_prompt_rename(const char title[], const char message[],
-		const struct response_variant *variants);
-static char options_prompt_rename_rec(const char title[], const char message[],
-		const struct response_variant *variants);
-static char options_prompt_overwrite(const char title[], const char message[],
-		const struct response_variant *variants);
-static char options_prompt_abort(const char title[], const char message[],
-		const struct response_variant *variants);
-static char options_prompt_skip_all(const char title[], const char message[],
-		const struct response_variant *variants);
-static char options_prompt_compare(const char title[], const char message[],
-		const struct response_variant *variants);
-static char cm_overwrite(const char title[], const char message[],
-		const struct response_variant *variants);
-static char cm_no(const char title[], const char message[],
-		const struct response_variant *variants);
-static char cm_skip(const char title[], const char message[],
-		const struct response_variant *variants);
+		fo_prompt_cb cb, void *cb_arg, fo_complete_cmd_func complete, int allow_ee);
+static char options_prompt_rename(const custom_prompt_t *details);
+static char options_prompt_rename_rec(const custom_prompt_t *details);
+static char options_prompt_overwrite(const custom_prompt_t *details);
+static char options_prompt_abort(const custom_prompt_t *details);
+static char options_prompt_skip_all(const custom_prompt_t *details);
+static char cm_overwrite(const custom_prompt_t *details);
+static char cm_no(const custom_prompt_t *details);
+static char cm_skip(const custom_prompt_t *details);
 static void parent_overwrite_with_put(int move);
 static void double_clash_with_put(int move);
 
@@ -76,45 +66,43 @@ TEARDOWN()
 
 static void
 line_prompt(const char prompt[], const char filename[], fo_prompt_cb cb,
-		fo_complete_cmd_func complete, int allow_ee)
+		void *cb_arg, fo_complete_cmd_func complete, int allow_ee)
 {
-	cb("b");
+	cb("b", cb_arg);
 }
 
 static void
 line_prompt_rec(const char prompt[], const char filename[], fo_prompt_cb cb,
-		fo_complete_cmd_func complete, int allow_ee)
+		void *cb_arg, fo_complete_cmd_func complete, int allow_ee)
 {
 	rename_cb = cb;
 }
 
 static char
-options_prompt_rename(const char title[], const char message[],
-		const struct response_variant *variants)
+options_prompt_rename(const custom_prompt_t *details)
 {
 	fops_init(&line_prompt, &options_prompt_overwrite);
 	return 'r';
 }
 
 static char
-options_prompt_rename_rec(const char title[], const char message[],
-		const struct response_variant *variants)
+options_prompt_rename_rec(const custom_prompt_t *details)
 {
 	fops_init(&line_prompt_rec, &options_prompt_overwrite);
 	return 'r';
 }
 
 static char
-options_prompt_overwrite(const char title[], const char message[],
-		const struct response_variant *variants)
+options_prompt_overwrite(const custom_prompt_t *details)
 {
 	return 'o';
 }
 
 static char
-options_prompt_abort(const char title[], const char message[],
-		const struct response_variant *variants)
+options_prompt_abort(const custom_prompt_t *details)
 {
+	const response_variant *variants = details->variants;
+
 	options_count = 0;
 	while(variants->key != '\0')
 	{
@@ -126,39 +114,27 @@ options_prompt_abort(const char title[], const char message[],
 }
 
 static char
-options_prompt_skip_all(const char title[], const char message[],
-		const struct response_variant *variants)
+options_prompt_skip_all(const custom_prompt_t *details)
 {
 	return 'S';
 }
 
 static char
-options_prompt_compare(const char title[], const char message[],
-		const struct response_variant *variants)
-{
-	fops_init(NULL, &options_prompt_abort);
-	return 'c';
-}
-
-static char
-cm_overwrite(const char title[], const char message[],
-		const struct response_variant *variants)
+cm_overwrite(const custom_prompt_t *details)
 {
 	fops_init(&line_prompt, &cm_no);
 	return 'o';
 }
 
 static char
-cm_no(const char title[], const char message[],
-		const struct response_variant *variants)
+cm_no(const custom_prompt_t *details)
 {
 	fops_init(&line_prompt, &cm_skip);
 	return 'n';
 }
 
 static char
-cm_skip(const char title[], const char message[],
-		const struct response_variant *variants)
+cm_skip(const custom_prompt_t *details)
 {
 	fops_init(&line_prompt, &options_prompt_overwrite);
 	return 's';
@@ -359,7 +335,7 @@ TEST(rename_on_put)
 	fops_init(&line_prompt_rec, &options_prompt_rename_rec);
 	(void)fops_put(&lwin, -1, 'a', 0);
 	/* Continue the operation. */
-	rename_cb("b");
+	rename_cb("b", /*arg=*/NULL);
 
 	restore_cwd(saved_cwd);
 	saved_cwd = save_cwd();
@@ -674,24 +650,6 @@ TEST(cursor_is_moved_even_if_no_file_was_processed)
 	assert_success(unlink(SANDBOX_PATH "/a"));
 }
 
-TEST(files_can_be_diffed)
-{
-	create_file(SANDBOX_PATH "/a");
-	create_dir(SANDBOX_PATH "/dir");
-	create_file(SANDBOX_PATH "/dir/a");
-
-	assert_success(regs_append('a', SANDBOX_PATH "/dir/a"));
-
-	fops_init(&line_prompt, &options_prompt_compare);
-	(void)fops_put(&lwin, -1, 'a', 0);
-	restore_cwd(saved_cwd);
-	saved_cwd = save_cwd();
-
-	assert_success(unlink(SANDBOX_PATH "/a"));
-	assert_success(unlink(SANDBOX_PATH "/dir/a"));
-	assert_success(rmdir(SANDBOX_PATH "/dir"));
-}
-
 TEST(show_merge_all_option_if_paths_include_dir)
 {
 	char path[PATH_MAX + 1];
@@ -711,7 +669,7 @@ TEST(show_merge_all_option_if_paths_include_dir)
 
 	options_count = 0;
 	(void)fops_put(&lwin, -1, 'a', 0);
-	assert_int_equal(9, options_count);
+	assert_int_equal(8, options_count);
 
 	restore_cwd(saved_cwd);
 	saved_cwd = save_cwd();
@@ -721,7 +679,7 @@ TEST(show_merge_all_option_if_paths_include_dir)
 
 	options_count = 0;
 	(void)fops_put(&lwin, -1, 'a', 0);
-	assert_int_equal(10, options_count);
+	assert_int_equal(9, options_count);
 
 	restore_cwd(saved_cwd);
 	saved_cwd = save_cwd();
@@ -750,7 +708,7 @@ TEST(no_merge_options_on_putting_links)
 
 	options_count = 0;
 	(void)fops_put_links(&lwin, 'a', 0);
-	assert_int_equal(8, options_count);
+	assert_int_equal(7, options_count);
 
 	restore_cwd(saved_cwd);
 	saved_cwd = save_cwd();
diff --git a/tests/fileops/rename_files.c b/tests/fileops/rename_files.c
index 3482c13..3fa2171 100644
--- a/tests/fileops/rename_files.c
+++ b/tests/fileops/rename_files.c
@@ -19,7 +19,7 @@
 #include "../../src/status.h"
 
 static void broken_link_name(const char prompt[], const char filename[],
-		fo_prompt_cb cb, fo_complete_cmd_func complete, int allow_ee);
+		fo_prompt_cb cb, void *cb_arg, fo_complete_cmd_func complete, int allow_ee);
 
 static char *saved_cwd;
 
@@ -191,9 +191,9 @@ TEST(rename_to_broken_symlink_name, IF(not_windows))
 
 static void
 broken_link_name(const char prompt[], const char filename[], fo_prompt_cb cb,
-		fo_complete_cmd_func complete, int allow_ee)
+		void *cb_arg, fo_complete_cmd_func complete, int allow_ee)
 {
-	cb("broken-link");
+	cb("broken-link", cb_arg);
 }
 
 TEST(file_list_can_be_edited_including_long_fnames, IF(not_windows))
diff --git a/tests/fileops/size.c b/tests/fileops/size.c
index 7ac4569..bd4c11d 100644
--- a/tests/fileops/size.c
+++ b/tests/fileops/size.c
@@ -24,11 +24,13 @@ SETUP()
 {
 	stats_init(&cfg);
 	view_setup(&lwin);
+	view_setup(&rwin);
 }
 
 TEARDOWN()
 {
 	view_teardown(&lwin);
+	view_teardown(&rwin);
 
 	stats_reset(&cfg);
 }
diff --git a/tests/fileops/suite.c b/tests/fileops/suite.c
index f0e741f..3546ceb 100644
--- a/tests/fileops/suite.c
+++ b/tests/fileops/suite.c
@@ -7,6 +7,7 @@
 
 #include "../../src/cfg/config.h"
 #include "../../src/ui/ui.h"
+#include "../../src/background.h"
 #include "../../src/undo.h"
 
 static OpsResult exec_func(OPS op, void *data, const char src[],
@@ -20,6 +21,7 @@ DEFINE_SUITE();
 SETUP_ONCE()
 {
 	stub_colmgr();
+	assert_success(bg_init());
 
 	cfg.sizefmt.ieci_prefixes = 0;
 	cfg.sizefmt.base = 1024;
diff --git a/tests/iop/cp.c b/tests/iop/cp.c
index 55cc283..0ead982 100644
--- a/tests/iop/cp.c
+++ b/tests/iop/cp.c
@@ -416,6 +416,29 @@ TEST(dir_symlink_copy_is_symlink, IF(not_windows))
 	}
 }
 
+TEST(overwrite_removal_handles_errors, IF(regular_unix_user))
+{
+	io_args_t args = {
+		.arg1.src = TEST_DATA_PATH "/read/two-lines",
+		.arg2.dst = SANDBOX_PATH "/file",
+		.arg3.crs = IO_CRS_REPLACE_FILES,
+	};
+	ioe_errlst_init(&args.result.errors);
+
+	create_test_file(SANDBOX_PATH "/file");
+
+	assert_success(chmod(SANDBOX_PATH, 0000));
+	assert_int_equal(IO_RES_FAILED, iop_cp(&args));
+	assert_success(chmod(SANDBOX_PATH, 0777));
+
+	assert_int_equal(1, args.result.errors.error_count);
+	assert_string_equal("Failed to unlink file",
+			args.result.errors.errors[0].msg);
+	ioe_errlst_free(&args.result.errors);
+
+	remove_file(SANDBOX_PATH "/file");
+}
+
 /* Windows lacks definitions of some declarations. */
 #ifndef _WIN32
 
diff --git a/tests/iop/error.c b/tests/iop/error.c
index b155d0d..cff8b69 100644
--- a/tests/iop/error.c
+++ b/tests/iop/error.c
@@ -51,7 +51,7 @@ TEST(file_removal_error_is_reported_and_logged_once)
 	};
 
 	retry_count = 2;
-	assert_int_equal(IO_RES_FAILED, iop_rmfile(&args));
+	assert_int_equal(IO_RES_ABORTED, iop_rmfile(&args));
 	assert_int_equal(0, retry_count);
 
 	/* There are two retry requests, but only one error in the log. */
@@ -69,7 +69,7 @@ TEST(dir_removal_error_is_reported_and_logged_once)
 	};
 
 	retry_count = 2;
-	assert_int_equal(IO_RES_FAILED, iop_rmdir(&args));
+	assert_int_equal(IO_RES_ABORTED, iop_rmdir(&args));
 	assert_int_equal(0, retry_count);
 
 	/* There are two retry requests, but only one error in the log. */
@@ -139,7 +139,7 @@ TEST(retry_does_not_mess_up_estimations, IF(not_windows))
 	assert_int_equal(9, args.estim->total_bytes);
 
 	retry_count = 2;
-	assert_int_equal(IO_RES_FAILED, iop_rmfile(&args));
+	assert_int_equal(IO_RES_ABORTED, iop_rmfile(&args));
 
 	assert_int_equal(1, args.estim->total_items);
 	assert_int_equal(1, args.estim->current_item);
diff --git a/tests/ior/error.c b/tests/ior/error.c
index 164399a..20abe12 100644
--- a/tests/ior/error.c
+++ b/tests/ior/error.c
@@ -32,7 +32,7 @@ TEST(file_removal_error_is_reported_and_logged_once, IF(not_windows))
 		ioe_errlst_init(&args.result.errors);
 
 		ignore_count = 0;
-		assert_int_equal(IO_RES_FAILED, ior_rm(&args));
+		assert_int_equal(IO_RES_ABORTED, ior_rm(&args));
 		assert_int_equal(0, ignore_count);
 
 		/* Second error must be about failure to remove directory, first one is
diff --git a/tests/keys/foreign_keys.c b/tests/keys/foreign_keys.c
index 3549ba1..a0199db 100644
--- a/tests/keys/foreign_keys.c
+++ b/tests/keys/foreign_keys.c
@@ -105,6 +105,20 @@ TEST(add_foreign_key_with_multikey)
 	assert_int_equal(L'4', multikey);
 }
 
+TEST(foreign_key_with_selector_can_be_redefined)
+{
+	key_conf_t key = { { &key_X }, .followed = FOLLOWED_BY_SELECTOR };
+	assert_success(vle_keys_foreign_add(L"X", &key, /*is_selector=*/0,
+				NORMAL_MODE));
+	assert_true(vle_keys_user_exists(L"X", NORMAL_MODE));
+
+	vle_keys_user_add(L"X", L"j", NORMAL_MODE, KEYS_FLAG_NONE);
+
+	last = 0;
+	assert_false(IS_KEYS_RET_CODE(vle_keys_exec(L"X")));
+	assert_int_equal(2, last);
+}
+
 static void
 key_X(key_info_t key_info, keys_info_t *keys_info)
 {
diff --git a/tests/keys/listing.c b/tests/keys/listing.c
index 52eb8e5..c579056 100644
--- a/tests/keys/listing.c
+++ b/tests/keys/listing.c
@@ -24,17 +24,17 @@ SETUP()
 TEST(normal_mode_keys)
 {
 	vle_keys_list(NORMAL_MODE, &process_listing, 0);
-	assert_int_equal(24, nitems);
+	assert_int_equal(1 + /*user=*/4 + 2 + /*builtin=*/19, nitems);
 
 	nitems = 0;
 	vle_keys_list(NORMAL_MODE, &process_listing, 1);
-	assert_int_equal(7, nitems);
+	assert_int_equal(1 + /*user=*/4 + 2, nitems);
 }
 
 TEST(visual_mode_keys)
 {
 	vle_keys_list(VISUAL_MODE, &process_listing, 0);
-	assert_int_equal(7, nitems);
+	assert_int_equal(1 + 2 + /*builtin=*/4, nitems);
 }
 
 static void
diff --git a/tests/keys/suite.c b/tests/keys/suite.c
index 06d35be..058546d 100644
--- a/tests/keys/suite.c
+++ b/tests/keys/suite.c
@@ -12,10 +12,11 @@ DEFINE_SUITE();
 
 SETUP()
 {
-	static int mode_flags[] = {
-		MF_USES_REGS | MF_USES_COUNT,
-		MF_USES_INPUT,
-		MF_USES_COUNT
+	static int mode_flags[MODES_COUNT] = {
+		MF_USES_REGS | MF_USES_COUNT, /* NORMAL_MODE */
+		MF_USES_INPUT,                /* CMDLINE_MODE */
+		MF_USES_INPUT,                /* NAV_MODE */
+		MF_USES_COUNT                 /* VISUAL_MODE */
 	};
 
 	vle_keys_init(MODES_COUNT, mode_flags, &silence);
diff --git a/tests/lua/api.c b/tests/lua/api.c
index 909459d..317fe7e 100644
--- a/tests/lua/api.c
+++ b/tests/lua/api.c
@@ -1,11 +1,17 @@
 #include <stic.h>
 
 #include "../../src/cfg/config.h"
+#include "../../src/engine/keys.h"
+#include "../../src/engine/variables.h"
 #include "../../src/lua/vlua.h"
+#include "../../src/modes/modes.h"
+#include "../../src/modes/wk.h"
 #include "../../src/ui/statusbar.h"
 #include "../../src/ui/ui.h"
 #include "../../src/utils/str.h"
+#include "../../src/utils/utils.h"
 #include "../../src/cmd_core.h"
+#include "../../src/event_loop.h"
 #include "../../src/status.h"
 
 #include <test-utils.h>
@@ -36,6 +42,23 @@ TEST(print_outputs_to_statusbar)
 	assert_string_equal("arg1\targ2", ui_sb_last());
 }
 
+TEST(os_getenv_works)
+{
+	init_variables();
+
+	ui_sb_msg("");
+	assert_success(vlua_run_string(vlua, "print(os.getenv('VIFM_TEST'))"));
+	assert_string_equal("nil", ui_sb_last());
+
+	assert_success(let_variables("$VIFM_TEST='test'"));
+
+	ui_sb_msg("");
+	assert_success(vlua_run_string(vlua, "print(os.getenv('VIFM_TEST'))"));
+	assert_string_equal("test", ui_sb_last());
+
+	clear_variables();
+}
+
 TEST(vifm_errordialog)
 {
 	assert_failure(vlua_run_string(vlua, "vifm.errordialog('title')"));
@@ -61,7 +84,8 @@ TEST(vifm_escape)
 
 	assert_success(vlua_run_string(vlua,
 				"print(vifm.escape(' '))"));
-	assert_string_equal("\" \"", ui_sb_last());
+	assert_string_equal(get_env_type() == ET_UNIX ? "\\ " : "\" \"",
+			ui_sb_last());
 }
 
 TEST(vifm_exists)
@@ -83,6 +107,34 @@ TEST(vifm_exists)
 	assert_string_equal("y", ui_sb_last());
 }
 
+TEST(vifm_executable)
+{
+	const char *const exec_file = SANDBOX_PATH "/exec" EXE_SUFFIX;
+	create_executable(exec_file);
+
+	ui_sb_msg("");
+
+	assert_success(vlua_run_string(vlua,
+				"print(vifm.executable('.') and 'y' or 'n')"));
+	assert_string_equal("n", ui_sb_last());
+
+	assert_success(vlua_run_string(vlua,
+				"print(vifm.executable('" SANDBOX_PATH "') and 'y' or 'n')"));
+	assert_string_equal("n", ui_sb_last());
+
+	assert_success(vlua_run_string(vlua,
+				"print(vifm.executable('" SANDBOX_PATH "/exec" EXE_SUFFIX "') "
+				      "and 'y' or 'n')"));
+	assert_string_equal("y", ui_sb_last());
+
+	assert_success(vlua_run_string(vlua,
+				"print(vifm.executable('" TEST_DATA_PATH "/read/two-lines') "
+				      "and 'y' or 'n')"));
+	assert_string_equal("n", ui_sb_last());
+
+	assert_success(remove(exec_file));
+}
+
 TEST(vifm_makepath)
 {
 	assert_success(vlua_run_string(vlua, "sandbox = '" SANDBOX_PATH "'"));
@@ -163,7 +215,7 @@ TEST(sb_quick_message_is_not_stored)
 
 TEST(vifm_currview)
 {
-	init_commands();
+	cmds_init();
 
 	conf_setup();
 	cfg.pane_tabs = 0;
@@ -198,16 +250,16 @@ TEST(vifm_currview)
 	++curr_view->id;
 	ui_sb_msg("");
 	assert_failure(vlua_run_string(vlua, "r:cd('bla')"));
-	assert_true(ends_with(ui_sb_last(),
-				"Invalid VifmView object (associated view is dead)"));
+	assert_string_ends_with("Invalid VifmView object (associated view is dead)",
+			ui_sb_last());
 
-	assert_success(exec_command("tabnew", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch1("tabnew", curr_view, CIT_COMMAND));
 
 	ui_sb_msg("");
 	assert_success(vlua_run_string(vlua, "l:cd('/')"));
 	assert_string_equal("", ui_sb_last());
 
-	assert_success(exec_command("tabonly", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch1("tabonly", curr_view, CIT_COMMAND));
 
 	columns_teardown();
 	opt_handlers_teardown();
@@ -223,10 +275,42 @@ TEST(vifm_version)
 				"print(vifm.version.api.has('feature'))"));
 	assert_string_equal("false", ui_sb_last());
 
+	ui_sb_msg("");
+	assert_success(vlua_run_string(vlua,
+				"print(vifm.version.api.major,"
+				"      vifm.version.api.minor,"
+				"      vifm.version.api.patch)"));
+	assert_string_equal("0\t1\t0", ui_sb_last());
+
 	ui_sb_msg("");
 	assert_success(vlua_run_string(vlua,
 				"print(vifm.version.api.atleast(0, 0, 0))"));
 	assert_string_equal("true", ui_sb_last());
+
+	ui_sb_msg("");
+	assert_success(vlua_run_string(vlua,
+				"print(vifm.version.api.atleast(0, 0, 1))"));
+	assert_string_equal("true", ui_sb_last());
+
+	ui_sb_msg("");
+	assert_success(vlua_run_string(vlua,
+				"print(vifm.version.api.atleast(0, 1))"));
+	assert_string_equal("true", ui_sb_last());
+
+	ui_sb_msg("");
+	assert_success(vlua_run_string(vlua,
+				"print(vifm.version.api.atleast(0, 1, 1))"));
+	assert_string_equal("false", ui_sb_last());
+
+	ui_sb_msg("");
+	assert_success(vlua_run_string(vlua,
+				"print(vifm.version.api.atleast(0, 2))"));
+	assert_string_equal("false", ui_sb_last());
+
+	ui_sb_msg("");
+	assert_success(vlua_run_string(vlua,
+				"print(vifm.version.api.atleast(1))"));
+	assert_string_equal("false", ui_sb_last());
 }
 
 TEST(vifm_run)
@@ -238,8 +322,8 @@ TEST(vifm_run)
 				"print(vifm.run({ cmd = 'exit 3',"
 				                " usetermmux = false,"
 				                " pause = 'unknown' }))"));
-	assert_true(ends_with(ui_sb_last(),
-				": Unrecognized value for `pause`: unknown"));
+	assert_string_ends_with(": Unrecognized value for `pause`: unknown",
+			ui_sb_last());
 
 	/* This waits for user input on Windows. */
 #ifndef _WIN32
@@ -268,5 +352,49 @@ TEST(vifm_run)
 	conf_teardown();
 }
 
+TEST(vifm_input)
+{
+	view_setup(&lwin);
+	view_setup(&rwin);
+
+	cfg.timeout_len = 1;
+
+	modes_init();
+
+	ui_sb_msg("");
+
+	/* Preparing input beforehand, because input() runs nested event loop. */
+	feed_keys(L"def" WK_C_m);
+	assert_success(vlua_run_string(vlua,
+				"print(vifm.input({ prompt = 'write: ',"
+				                  " initial = 'abc',"
+				                  " complete = 'dir' }))"));
+	assert_string_equal("abcdef", ui_sb_last());
+
+	/* Preparing input beforehand, because input() runs nested event loop. */
+	feed_keys(L"xyz" WK_C_m);
+	assert_success(vlua_run_string(vlua,
+				"print(vifm.input({ prompt = 'write: ',"
+				                  " complete = 'file' }))"));
+	assert_string_equal("xyz", ui_sb_last());
+
+	/* Preparing input beforehand, because input() runs nested event loop. */
+	feed_keys(WK_C_c);
+	assert_success(vlua_run_string(vlua,
+				"print(vifm.input({ prompt = 'write:' }))"));
+	assert_string_equal("nil", ui_sb_last());
+
+	assert_failure(vlua_run_string(vlua,
+				"print(vifm.input({ prompt = 'write: ',"
+				                  " complete = 'bla' }))"));
+	assert_string_ends_with(": Unrecognized value for `complete`: bla",
+			ui_sb_last());
+
+	vle_keys_reset();
+
+	view_teardown(&lwin);
+	view_teardown(&rwin);
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 : */
diff --git a/tests/lua/api_cmds.c b/tests/lua/api_cmds.c
index 1ac00f5..9bdf1fb 100644
--- a/tests/lua/api_cmds.c
+++ b/tests/lua/api_cmds.c
@@ -5,7 +5,6 @@
 #include "../../src/lua/vlua.h"
 #include "../../src/ui/statusbar.h"
 #include "../../src/ui/ui.h"
-#include "../../src/utils/str.h"
 #include "../../src/cmd_core.h"
 #include "../../src/status.h"
 
@@ -36,7 +35,7 @@ TEARDOWN()
 
 TEST(cmds_add)
 {
-	init_commands();
+	cmds_init();
 
 	ui_sb_msg("");
 
@@ -62,32 +61,32 @@ TEST(cmds_add)
 	assert_failure(vlua_run_string(vlua, "vifm.cmds.add {"
 	                                     "  name = 'cmd'"
 	                                     "}"));
-	assert_true(ends_with(ui_sb_last(), "`handler` key is mandatory"));
+	assert_string_ends_with("`handler` key is mandatory", ui_sb_last());
 
 	assert_failure(vlua_run_string(vlua, "vifm.cmds.add {"
 	                                     "  handler = handler"
 	                                     "}"));
-	assert_true(ends_with(ui_sb_last(), "`name` key is mandatory"));
+	assert_string_ends_with("`name` key is mandatory", ui_sb_last());
 
 	assert_failure(vlua_run_string(vlua, "vifm.cmds.add {"
 	                                     "  name = 'cmd',"
 	                                     "  handler = 10"
 	                                     "}"));
-	assert_true(ends_with(ui_sb_last(), "`handler` value must be a function"));
+	assert_string_ends_with("`handler` value must be a function", ui_sb_last());
 
 	assert_failure(vlua_run_string(vlua, "vifm.cmds.add {"
 	                                     "  name = 'cmd',"
 	                                     "  handler = handler,"
 	                                     "  minargs = 'min'"
 	                                     "}"));
-	assert_true(ends_with(ui_sb_last(), "`minargs` value must be a number"));
+	assert_string_ends_with("`minargs` value must be a number", ui_sb_last());
 
 	assert_failure(vlua_run_string(vlua, "vifm.cmds.add {"
 	                                     "  name = 'cmd',"
 	                                     "  handler = handler,"
 	                                     "  maxargs = 'max'"
 	                                     "}"));
-	assert_true(ends_with(ui_sb_last(), "`maxargs` value must be a number"));
+	assert_string_ends_with("`maxargs` value must be a number", ui_sb_last());
 
 	ui_sb_msg("");
 	assert_success(vlua_run_string(vlua, "vifm.cmds.add {"
@@ -99,7 +98,7 @@ TEST(cmds_add)
 	assert_string_equal("", ui_sb_last());
 
 	ui_sb_msg("");
-	assert_failure(exec_command("cmd arg", curr_view, CIT_COMMAND));
+	assert_failure(cmds_dispatch1("cmd arg", curr_view, CIT_COMMAND));
 	assert_string_equal("msg", ui_sb_last());
 	assert_int_equal(1, curr_stats.save_msg);
 
@@ -112,9 +111,9 @@ TEST(cmds_add)
 	assert_string_equal("", ui_sb_last());
 
 	ui_sb_msg("");
-	assert_failure(exec_command("bcmd", curr_view, CIT_COMMAND));
-	assert_true(ends_with(ui_sb_last(),
-				": attempt to call a nil value (global 'adsf')"));
+	assert_failure(cmds_dispatch1("bcmd", curr_view, CIT_COMMAND));
+	assert_string_ends_with(": attempt to call a nil value (global 'adsf')",
+			ui_sb_last());
 
 	ui_sb_msg("");
 	assert_success(vlua_run_string(vlua, "vifm.cmds.add {"
@@ -126,7 +125,7 @@ TEST(cmds_add)
 	assert_string_equal("", ui_sb_last());
 
 	ui_sb_msg("");
-	assert_failure(exec_command("cmdinf arg1 arg2", curr_view, CIT_COMMAND));
+	assert_failure(cmds_dispatch1("cmdinf arg1 arg2", curr_view, CIT_COMMAND));
 	assert_string_equal("msg", ui_sb_last());
 	assert_int_equal(1, curr_stats.save_msg);
 }
@@ -138,7 +137,7 @@ TEST(cmds_command)
 	                                     "  name = 'name',"
 	                                     "  action = ''"
 	                                     "}"));
-	assert_true(ends_with(ui_sb_last(), "Action can't be empty"));
+	assert_string_ends_with("Action can't be empty", ui_sb_last());
 
 	ui_sb_msg("");
 	assert_success(vlua_run_string(vlua, "r = vifm.cmds.command({"
diff --git a/tests/lua/api_events.c b/tests/lua/api_events.c
new file mode 100644
index 0000000..8081109
--- /dev/null
+++ b/tests/lua/api_events.c
@@ -0,0 +1,119 @@
+#include <stic.h>
+
+#include "../../src/cfg/config.h"
+#include "../../src/compat/os.h"
+#include "../../src/lua/vlua.h"
+#include "../../src/ui/statusbar.h"
+#include "../../src/ops.h"
+#include "../../src/status.h"
+
+#include <test-utils.h>
+
+static vlua_t *vlua;
+
+SETUP()
+{
+	vlua = vlua_init();
+}
+
+TEARDOWN()
+{
+	vlua_finish(vlua);
+}
+
+TEST(bad_event_name)
+{
+	ui_sb_msg("");
+	assert_failure(vlua_run_string(vlua,
+	      "vifm.events.listen {"
+	      "  event = 'random',"
+	      "  handler = function() end"
+	      "}"));
+	assert_string_ends_with(": No such event: random", ui_sb_last());
+}
+
+TEST(app_exit_is_called)
+{
+	assert_success(vlua_run_string(vlua,
+	      "i = 0\n"
+	      "vifm.events.listen {"
+	      "  event = 'app.exit',"
+	      "  handler = function() i = i + 1 end"
+	      "}"));
+
+	vlua_events_app_exit(vlua);
+	vlua_process_callbacks(vlua);
+
+	ui_sb_msg("");
+	assert_success(vlua_run_string(vlua, "print(i)"));
+	assert_string_equal("1", ui_sb_last());
+}
+
+TEST(app_fsop_is_called)
+{
+	assert_success(vlua_run_string(vlua,
+	      "data = {}\n"
+	      "vifm.events.listen {"
+	      "  event = 'app.fsop',"
+	      "  handler = function(info) data[#data + 1] = info end"
+	      "}"));
+
+	conf_setup();
+	cfg.use_system_calls = 1;
+	curr_stats.vlua = vlua;
+	assert_success(os_chdir(SANDBOX_PATH));
+	assert_int_equal(OPS_SUCCEEDED, perform_operation(OP_USR, NULL, NULL,
+				"echo", NULL));
+	assert_int_equal(OPS_SUCCEEDED, perform_operation(OP_MKFILE, NULL, NULL,
+				"from", NULL));
+	assert_int_equal(OPS_SUCCEEDED, perform_operation(OP_MOVE, NULL, NULL,
+				"from", "to"));
+	assert_int_equal(OPS_SUCCEEDED, perform_operation(OP_REMOVESL, NULL, NULL,
+				"to", NULL));
+	vlua_process_callbacks(vlua);
+	curr_stats.vlua = NULL;
+	cfg.use_system_calls = 0;
+	conf_teardown();
+
+	ui_sb_msg("");
+	assert_success(vlua_run_string(vlua, "print(#data)"));
+	assert_string_equal("3", ui_sb_last());
+	assert_success(vlua_run_string(vlua, "print(data[1].op,"
+	                                           "data[1].path,"
+	                                           "data[1].target,"
+	                                           "data[1].isdir)"));
+	assert_string_equal("create\tfrom\tnil\tfalse", ui_sb_last());
+	assert_success(vlua_run_string(vlua, "print(data[2].op,"
+	                                           "data[2].path,"
+	                                           "data[2].target,"
+	                                           "data[2].fromtrash,"
+	                                           "data[2].totrash,"
+	                                           "data[2].isdir)"));
+	assert_string_equal("move\tfrom\tto\tfalse\tfalse\tfalse", ui_sb_last());
+	assert_success(vlua_run_string(vlua, "print(data[3].op,"
+	                                           "data[3].path,"
+	                                           "data[3].target,"
+	                                           "data[3].isdir)"));
+	assert_string_equal("remove\tto\tnil\tfalse", ui_sb_last());
+}
+
+TEST(same_handler_is_called_once)
+{
+	ui_sb_msg("");
+
+	assert_success(vlua_run_string(vlua,
+	      "i = 0\n"
+	      "handler = function() i = i + 1 end\n"
+	      "vifm.events.listen { event = 'app.exit', handler = handler }"
+	      "vifm.events.listen { event = 'app.exit', handler = handler }"));
+
+	vlua_events_app_exit(vlua);
+	vlua_process_callbacks(vlua);
+	assert_string_equal("", ui_sb_last());
+
+	assert_success(vlua_run_string(vlua, "print(i)"));
+	assert_string_equal("1", ui_sb_last());
+}
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 : */
diff --git a/tests/lua/api_handlers.c b/tests/lua/api_handlers.c
index 4d83fc6..07de0e1 100644
--- a/tests/lua/api_handlers.c
+++ b/tests/lua/api_handlers.c
@@ -5,7 +5,6 @@
 #include "../../src/ui/statusbar.h"
 #include "../../src/ui/quickview.h"
 #include "../../src/ui/ui.h"
-#include "../../src/utils/str.h"
 #include "../../src/utils/string_array.h"
 #include "../../src/utils/utils.h"
 #include "../../src/cmd_completion.h"
@@ -55,13 +54,13 @@ TEST(bad_args)
 	assert_failure(vlua_run_string(vlua,
 				"print(vifm.addhandler{ name = nil,"
 				                      " handler = nil })"));
-	assert_true(ends_with(ui_sb_last(), ": `name` key is mandatory"));
+	assert_string_ends_with(": `name` key is mandatory", ui_sb_last());
 
 	ui_sb_msg("");
 	assert_failure(vlua_run_string(vlua,
 				"print(vifm.addhandler{ name = 'NAME',"
 				                      " handler = nil })"));
-	assert_true(ends_with(ui_sb_last(), ": `handler` key is mandatory"));
+	assert_string_ends_with(": `handler` key is mandatory", ui_sb_last());
 }
 
 TEST(bad_name)
@@ -72,14 +71,14 @@ TEST(bad_name)
 	assert_failure(vlua_run_string(vlua,
 				"print(vifm.addhandler{ name = '',"
 				                      " handler = handler })"));
-	assert_true(ends_with(ui_sb_last(), ": Handler's name can't be empty"));
+	assert_string_ends_with(": Handler's name can't be empty", ui_sb_last());
 
 	ui_sb_msg("");
 	assert_failure(vlua_run_string(vlua,
 				"print(vifm.addhandler{ name = 'name with white\tspace',"
 				                      " handler = handler })"));
-	assert_true(ends_with(ui_sb_last(),
-				": Handler's name can't contain whitespace"));
+	assert_string_ends_with(": Handler's name can't contain whitespace",
+			ui_sb_last());
 }
 
 TEST(registered)
@@ -203,8 +202,8 @@ TEST(error_open_invocation)
 
 	ui_sb_msg("");
 	vlua_open_file(vlua, "#vifmtest#handle", &entry);
-	assert_true(ends_with(ui_sb_last(),
-				": attempt to call a nil value (global 'asdf')"));
+	assert_string_ends_with(": attempt to call a nil value (global 'asdf')",
+			ui_sb_last());
 }
 
 TEST(invalid_statusline_formatter)
@@ -225,8 +224,8 @@ TEST(error_statusline_formatter)
 	assert_string_equal("true", ui_sb_last());
 
 	char *format = vlua_make_status_line(vlua, "#vifmtest#handle", &lwin, 10);
-	assert_true(ends_with(format,
-				": attempt to call a nil value (global 'asdf')"));
+	assert_string_ends_with(": attempt to call a nil value (global 'asdf')",
+			format);
 	free(format);
 }
 
@@ -261,6 +260,26 @@ TEST(good_statusline_formatter)
 	free(format);
 }
 
+TEST(good_tabline_formatter)
+{
+	assert_success(vlua_run_string(vlua,
+				"function handle(info)"
+				"  return { format = 'width='..info.width"
+				"                  ..',other='..tostring(info.other) }"
+				"end"));
+
+	ui_sb_msg("");
+	assert_success(vlua_run_string(vlua,
+				"print(vifm.addhandler{ name = 'handle',"
+				                      " handler = handle })"));
+	assert_string_equal("true", ui_sb_last());
+
+	char *format =
+		vlua_make_tab_line(vlua, "#vifmtest#handle", /*other=*/1, /*width=*/11);
+	assert_string_equal("width=11,other=true", format);
+	free(format);
+}
+
 TEST(handlers_run_in_safe_mode)
 {
 	assert_success(vlua_run_string(vlua,
@@ -273,8 +292,8 @@ TEST(handlers_run_in_safe_mode)
 	assert_string_equal("true", ui_sb_last());
 
 	char *format = vlua_make_status_line(vlua, "#vifmtest#handle", &lwin, 10);
-	assert_true(ends_with(format,
-				": Unsafe functions can't be called in this environment!"));
+	assert_string_ends_with(
+			": Unsafe functions can't be called in this environment!", format);
 	free(format);
 }
 
@@ -294,8 +313,8 @@ TEST(error_editor_handler)
 	assert_string_equal("true", ui_sb_last());
 
 	assert_failure(vlua_edit_one(vlua, "#vifmtest#handle", "path", -1, -1, 0));
-	assert_true(ends_with(ui_sb_last(),
-				": attempt to call a nil value (global 'asdf')"));
+	assert_string_ends_with(": attempt to call a nil value (global 'asdf')",
+				ui_sb_last());
 }
 
 TEST(error_editor_handler_return)
@@ -341,15 +360,9 @@ TEST(good_editor_handler_return)
 
 TEST(open_help_input)
 {
-#ifndef _WIN32
-	char vimdoc_dir[PATH_MAX + 1] = PACKAGE_DATA_DIR "/vim-doc";
-#else
-	char exe_dir[PATH_MAX + 1];
-	(void)get_exe_dir(exe_dir, sizeof(exe_dir));
-
 	char vimdoc_dir[PATH_MAX + 1];
-	snprintf(vimdoc_dir, sizeof(vimdoc_dir), "%s/vim-doc", exe_dir);
-#endif
+	snprintf(vimdoc_dir, sizeof(vimdoc_dir), "%s/vim-doc",
+			get_installed_data_dir());
 
 	assert_success(vlua_run_string(vlua,
 				"function handle(info)"
diff --git a/tests/lua/api_jobs.c b/tests/lua/api_jobs.c
index 66481d5..87b501f 100644
--- a/tests/lua/api_jobs.c
+++ b/tests/lua/api_jobs.c
@@ -1,21 +1,29 @@
 #include <stic.h>
 
+#include <unistd.h> /* usleep() */
+
+#include "../../src/engine/var.h"
+#include "../../src/engine/variables.h"
 #include "../../src/lua/vlua.h"
 #include "../../src/ui/statusbar.h"
-#include "../../src/utils/str.h"
+#include "../../src/background.h"
 
 #include <test-utils.h>
 
+static void wait_for_job(void);
+
 static vlua_t *vlua;
 
 SETUP()
 {
+	conf_setup();
 	vlua = vlua_init();
 }
 
 TEARDOWN()
 {
 	vlua_finish(vlua);
+	conf_teardown();
 }
 
 TEST(vifmjob_bad_arg)
@@ -24,7 +32,7 @@ TEST(vifmjob_bad_arg)
 	assert_failure(vlua_run_string(vlua, "info = { cmd = 'echo ignored',"
 	                                     "         iomode = 'u' }\n"
 	                                     "job = vifm.startjob(info)"));
-	assert_true(ends_with(ui_sb_last(), "Unknown 'iomode' value: u"));
+	assert_string_ends_with("Unknown 'iomode' value: u", ui_sb_last());
 }
 
 /* This test comes before other good startjob tests to make it pass faster.
@@ -32,7 +40,6 @@ TEST(vifmjob_bad_arg)
  * from the process. */
 TEST(vifmjob_errors)
 {
-	conf_setup();
 	ui_sb_msg("");
 
 	assert_success(vlua_run_string(vlua, "info = { cmd = 'echo err 1>&2' }\n"
@@ -40,33 +47,25 @@ TEST(vifmjob_errors)
 	                                     "job:wait()\n"
 	                                     "while #job:errors() == 0 do end\n"
 	                                     "print(job:errors())"));
-	assert_true(starts_with_lit(ui_sb_last(), "err"));
+	assert_string_starts_with("err", ui_sb_last());
 
 	assert_success(vlua_run_string(vlua, "info = { cmd = 'echo out' }\n"
 	                                     "job = vifm.startjob(info)\n"
 	                                     "print(job:errors())"));
 	assert_string_equal("", ui_sb_last());
-
-	conf_teardown();
 }
 
 TEST(vifm_startjob)
 {
-	conf_setup();
-
 	ui_sb_msg("");
 	assert_success(vlua_run_string(vlua, "job = vifm.startjob({ cmd = 'echo' })\n"
 	                                     "job:stdout():lines()()\n"
 	                                     "print(job:exitcode())"));
 	assert_string_equal("0", ui_sb_last());
-
-	conf_teardown();
 }
 
 TEST(vifmjob_exitcode)
 {
-	conf_setup();
-
 	ui_sb_msg("");
 	assert_success(vlua_run_string(vlua, "info = {\n"
 	                                     "  cmd = 'exit 41',\n"
@@ -76,14 +75,10 @@ TEST(vifmjob_exitcode)
 	                                     "job = vifm.startjob(info)\n"
 	                                     "print(job:exitcode())"));
 	assert_string_equal("41", ui_sb_last());
-
-	conf_teardown();
 }
 
 TEST(vifmjob_stdin, IF(have_cat))
 {
-	conf_setup();
-
 	ui_sb_msg("");
 	assert_success(vlua_run_string(vlua,
 	      "info = { cmd = 'cat > " SANDBOX_PATH "/file', iomode = 'w' }\n"
@@ -97,8 +92,6 @@ TEST(vifmjob_stdin, IF(have_cat))
 	      "job:wait()"));
 	assert_string_equal("true", ui_sb_last());
 
-	conf_teardown();
-
 	const char *lines[] = { "text" };
 	file_is(SANDBOX_PATH "/file", lines, 1);
 	remove_file(SANDBOX_PATH "/file");
@@ -106,7 +99,9 @@ TEST(vifmjob_stdin, IF(have_cat))
 
 TEST(vifmjob_stdin_broken_pipe, IF(not_windows))
 {
-	conf_setup();
+	var_t var = var_from_int(0);
+	setvar("v:jobcount", var);
+	var_free(var);
 
 	ui_sb_msg("");
 	assert_success(vlua_run_string(vlua,
@@ -118,13 +113,21 @@ TEST(vifmjob_stdin_broken_pipe, IF(not_windows))
 	      "job:wait()"));
 	assert_string_equal("true", ui_sb_last());
 
-	conf_teardown();
+	/* Broken pipe + likely dead parent VifmJob object. */
+	ui_sb_msg("");
+	assert_success(vlua_run_string(vlua,
+	      "info = { cmd = 'no-such-command-exists', iomode = 'w' }"
+	      "stdin = vifm.startjob(info):stdin()"
+	      "vifm.startjob({ cmd = 'sleep 0.01' }):wait()"
+	      "print(stdin:write('text') == stdin)"));
+	assert_string_equal("true", ui_sb_last());
+	bg_check();
+	assert_failure(vlua_run_string(vlua, "print(stdin:write('text') == stdin)"));
+	assert_string_ends_with(": attempt to use a closed file", ui_sb_last());
 }
 
 TEST(vifmjob_stdout)
 {
-	conf_setup();
-
 	ui_sb_msg("");
 	assert_success(vlua_run_string(vlua, "info = { cmd = 'echo out' }\n"
 	                                     "job = vifm.startjob(info)\n"
@@ -133,15 +136,11 @@ TEST(vifmjob_stdout)
 	                                     "else\n"
 	                                     "  print(job:stdout():read('a'))\n"
 	                                     "end"));
-	assert_true(starts_with_lit(ui_sb_last(), "out"));
-
-	conf_teardown();
+	assert_string_starts_with("out", ui_sb_last());
 }
 
 TEST(vifmjob_stderr)
 {
-	conf_setup();
-
 	ui_sb_msg("");
 	assert_success(vlua_run_string(vlua, "info = { cmd = 'echo err 1>&2' }\n"
 	                                     "job = vifm.startjob(info)\n"
@@ -151,37 +150,94 @@ TEST(vifmjob_stderr)
 	                                     "         mergestreams = true }\n"
 	                                     "job = vifm.startjob(info)\n"
 	                                     "print(job:stdout():read('a'))"));
-	assert_true(starts_with_lit(ui_sb_last(), "err"));
-
-	conf_teardown();
+	assert_string_starts_with("err", ui_sb_last());
 }
 
 TEST(vifmjob_no_out)
 {
-	conf_setup();
-
 	ui_sb_msg("");
 	assert_failure(vlua_run_string(vlua, "info = { cmd = 'echo ignored',"
 	                                     "         iomode = '' }\n"
 	                                     "job = vifm.startjob(info)\n"
 	                                     "print(job:stdout() and 'FAIL')"));
-	assert_true(ends_with(ui_sb_last(), "The job has no output stream"));
-
-	conf_teardown();
+	assert_string_ends_with("The job has no output stream", ui_sb_last());
 }
 
 TEST(vifmjob_no_in)
 {
-	conf_setup();
-
 	ui_sb_msg("");
 	assert_failure(vlua_run_string(vlua, "info = { cmd = 'echo ignored',"
 	                                     "         iomode = '' }\n"
 	                                     "job = vifm.startjob(info)\n"
 	                                     "print(job:stdin() and 'FAIL')"));
-	assert_true(ends_with(ui_sb_last(), "The job has no input stream"));
+	assert_string_ends_with("The job has no input stream", ui_sb_last());
+}
 
-	conf_teardown();
+TEST(vifmjob_onexit_good)
+{
+	var_t var = var_from_int(0);
+	setvar("v:jobcount", var);
+	var_free(var);
+
+	ui_sb_msg("");
+
+	assert_success(vlua_run_string(vlua,
+	      "info = { cmd = 'echo hi',"
+	              " onexit = function(job) print(job:exitcode()) end }"
+	      "vifm.startjob(info)"));
+
+	wait_for_job();
+	vlua_process_callbacks(vlua);
+
+	assert_string_equal("0", ui_sb_last());
+}
+
+TEST(vifmjob_onexit_bad)
+{
+	var_t var = var_from_int(0);
+	setvar("v:jobcount", var);
+	var_free(var);
+
+	ui_sb_msg("");
+
+	assert_success(vlua_run_string(vlua,
+	      "info = { cmd = 'echo hi',"
+	              " onexit = function(job) fail_here() end }"
+	      "vifm.startjob(info)"));
+
+	wait_for_job();
+	vlua_process_callbacks(vlua);
+
+	assert_string_ends_with(": attempt to call a nil value (global 'fail_here')",
+			ui_sb_last());
+}
+
+static void
+wait_for_job(void)
+{
+	bg_job_t *job = bg_jobs;
+	assert_non_null(job);
+
+	bg_job_incref(job);
+
+	int counter = 0;
+	while(bg_job_is_running(job))
+	{
+		usleep(5000);
+		bg_check();
+		if(++counter > 100)
+		{
+			assert_fail("Waiting for too long.");
+			return;
+		}
+	}
+
+	/* When the job is marked as not running, the callback might not yet been
+	 * dispatched, so call bg_check() once again to be sure. */
+	bg_check();
+
+	assert_int_equal(0, job->exit_code);
+	bg_job_decref(job);
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/tests/lua/api_keys.c b/tests/lua/api_keys.c
index 3f8a0bc..70dd646 100644
--- a/tests/lua/api_keys.c
+++ b/tests/lua/api_keys.c
@@ -7,7 +7,6 @@
 #include "../../src/ui/statusbar.h"
 #include "../../src/ui/ui.h"
 #include "../../src/utils/dynarray.h"
-#include "../../src/utils/str.h"
 #include "../../src/bracket_notation.h"
 #include "../../src/modes/modes.h"
 #include "../../src/modes/wk.h"
@@ -43,7 +42,7 @@ SETUP()
 	lwin.dir_entry[1].name = strdup("file1");
 	lwin.dir_entry[1].origin = &lwin.curr_dir[0];
 
-	init_modes();
+	modes_init();
 	regs_init();
 }
 
@@ -64,27 +63,27 @@ TEST(keys_add_errors)
 	                                     "  modes = { 'cmdline', 'normal' },"
 	                                     "  handler = handler,"
 	                                     "}"));
-	assert_true(ends_with(ui_sb_last(), ": `shortcut` key is mandatory"));
+	assert_string_ends_with(": `shortcut` key is mandatory", ui_sb_last());
 
 	assert_failure(vlua_run_string(vlua, "vifm.keys.add {"
 	                                     "  shortcut = 'X',"
 	                                     "  handler = handler,"
 	                                     "}"));
-	assert_true(ends_with(ui_sb_last(), ": `modes` key is mandatory"));
+	assert_string_ends_with(": `modes` key is mandatory", ui_sb_last());
 
 	assert_failure(vlua_run_string(vlua, "vifm.keys.add {"
 	                                     "  shortcut = 'X',"
 	                                     "  modes = { 'cmdline', 'normal' },"
 	                                     "}"));
-	assert_true(ends_with(ui_sb_last(), ": `handler` key is mandatory"));
+	assert_string_ends_with(": `handler` key is mandatory", ui_sb_last());
 
 	assert_failure(vlua_run_string(vlua, "vifm.keys.add {"
 	                                     "  shortcut = '',"
 	                                     "  modes = { 'cmdline', 'normal' },"
 	                                     "  handler = handler,"
 	                                     "}"));
-	assert_true(ends_with(ui_sb_last(),
-				": Shortcut can't be empty or longer than 15"));
+	assert_string_ends_with(": Shortcut can't be empty or longer than 15",
+			ui_sb_last());
 
 	assert_failure(vlua_run_string(vlua, "vifm.keys.add {"
 	                                     "  shortcut = 'X',"
@@ -92,15 +91,15 @@ TEST(keys_add_errors)
 	                                     "  handler = handler,"
 	                                     "  followedby = 'something',"
 	                                     "}"));
-	assert_true(ends_with(ui_sb_last(),
-				": Unrecognized value for `followedby`: something"));
+	assert_string_ends_with(": Unrecognized value for `followedby`: something",
+			ui_sb_last());
 
 	assert_failure(vlua_run_string(vlua, "vifm.keys.add {"
 	                                     "  shortcut = 'X',"
 	                                     "  isselector = 10,"
 	                                     "}"));
-	assert_true(ends_with(ui_sb_last(),
-				": `isselector` value must be a boolean"));
+	assert_string_ends_with(": `isselector` value must be a boolean",
+			ui_sb_last());
 }
 
 TEST(keys_bad_key_handler)
@@ -117,8 +116,8 @@ TEST(keys_bad_key_handler)
 	assert_string_equal("true", ui_sb_last());
 
 	(void)vle_keys_exec_timed_out(WK_X);
-	assert_true(ends_with(ui_sb_last(),
-				": attempt to call a nil value (global 'adsf')"));
+	assert_string_ends_with(": attempt to call a nil value (global 'adsf')",
+			ui_sb_last());
 }
 
 TEST(keys_bad_selector_handler)
@@ -136,8 +135,8 @@ TEST(keys_bad_selector_handler)
 	assert_string_equal("true", ui_sb_last());
 
 	(void)vle_keys_exec_timed_out(L"yX");
-	assert_true(ends_with(ui_sb_last(),
-				": attempt to call a nil value (global 'adsf')"));
+	assert_string_ends_with(": attempt to call a nil value (global 'adsf')",
+			ui_sb_last());
 }
 
 TEST(keys_bad_selector_return)
@@ -181,7 +180,9 @@ TEST(keys_bad_selector_return_table)
 TEST(keys_bad_selector_index)
 {
 	assert_success(vlua_run_string(vlua, "function badhandler()\n"
-	                                     "  return { indexes = { 0 } }\n"
+	                                     "  return {"
+	                                     "    indexes = { 0, 'notint', 1.5 }"
+	                                     "  }\n"
 	                                     "end"));
 
 	assert_success(vlua_run_string(vlua, "print(vifm.keys.add {"
@@ -284,7 +285,7 @@ TEST(keys_add_selector)
 	assert_int_equal(1, curr_stats.save_msg);
 	assert_string_equal("2 files yanked", ui_sb_last());
 
-	reg_t *reg = regs_find(DEFAULT_REG_NAME);
+	const reg_t *reg = regs_find(DEFAULT_REG_NAME);
 	assert_non_null(reg);
 	assert_int_equal(2, reg->nfiles);
 	assert_string_equal("/lwin/file0", reg->files[0]);
@@ -297,9 +298,10 @@ TEST(keys_add_modes)
 
 	assert_success(vlua_run_string(vlua, "print(vifm.keys.add {"
 	                                     "    shortcut = 'X',"
-	                                     "    modes = { 'cmdline', 'normal', "
-	                                     "              'visual', 'menus', "
-	                                     "              'dialogs', 'view' },"
+	                                     "    modes = { 'cmdline', 'nav',"
+	                                     "              'normal', 'visual',"
+	                                     "              'menus', 'dialogs',"
+	                                     "              'view' },"
 	                                     "    handler = function() end,"
 	                                     "})"));
 	assert_string_equal("true", ui_sb_last());
diff --git a/tests/lua/api_opts.c b/tests/lua/api_opts.c
index 4e8cefa..bbd44e7 100644
--- a/tests/lua/api_opts.c
+++ b/tests/lua/api_opts.c
@@ -3,7 +3,6 @@
 #include "../../src/lua/vlua.h"
 #include "../../src/ui/statusbar.h"
 #include "../../src/ui/ui.h"
-#include "../../src/utils/str.h"
 
 #include <test-utils.h>
 
@@ -49,8 +48,9 @@ TEST(bad_option_value)
 {
 	ui_sb_msg("");
 	assert_failure(vlua_run_string(vlua, "vifm.opts.global.caseoptions = 'yes'"));
-	assert_true(ends_with(ui_sb_last(), "Illegal character: <y>\n"
-				"Failed to set value of option caseoptions"));
+	assert_string_ends_with(
+			"Illegal character: <y>\n" "Failed to set value of option caseoptions",
+			ui_sb_last());
 }
 
 TEST(local_option)
@@ -171,8 +171,8 @@ TEST(view_option)
 
 	assert_true(curr_view == &rwin);
 	assert_failure(vlua_run_string(vlua, "v.locopts.dotfiles = 'asdf'"));
-	assert_true(ends_with(ui_sb_last(),
-				"bad argument #3 to '?' (boolean expected, got string)"));
+	assert_string_ends_with(
+			"bad argument #3 to '?' (boolean expected, got string)", ui_sb_last());
 	assert_true(curr_view == &rwin);
 }
 
diff --git a/tests/lua/api_tabs.c b/tests/lua/api_tabs.c
index a2cb803..a0a7830 100644
--- a/tests/lua/api_tabs.c
+++ b/tests/lua/api_tabs.c
@@ -179,7 +179,8 @@ TEST(getname_global_tabs)
 	ui_sb_msg("");
 	assert_failure(vlua_run_string(vlua,
 				"print(vifm.tabs.get({ index = 0 }):getname())"));
-	assert_true(ends_with(ui_sb_last(), ": No tab with index -1 on active side"));
+	assert_string_ends_with(": No tab with index -1 on active side",
+			ui_sb_last());
 }
 
 TEST(getname_pane_tabs)
@@ -236,14 +237,13 @@ TEST(getview_errors)
 	ui_sb_msg("");
 	assert_failure(vlua_run_string(vlua,
 				"print(vifm.tabs.get({ index = 1 }):getview({ pane = 0 }).cwd)"));
-	assert_true(ends_with(ui_sb_last(),
-				": pane field is not in the range [1; 2]"));
+	assert_string_ends_with(": pane field is not in the range [1; 2]",
+			ui_sb_last());
 
 	ui_sb_msg("");
 	assert_failure(vlua_run_string(vlua,
 				"print(vifm.tabs.get({ index = 1 }):getview(0))"));
-	assert_true(ends_with(ui_sb_last(),
-				": Parameter #2 value must be a table"));
+	assert_string_ends_with(": Parameter #2 value must be a table", ui_sb_last());
 }
 
 TEST(getview_global_tabs)
@@ -264,18 +264,25 @@ TEST(getview_global_tabs)
 				"print(vifm.tabs.get({ index = 1 }):getview({ pane = 1 }).cwd)"));
 	assert_string_equal("t1vl", ui_sb_last());
 
+	/* Changing active pane should have no affect on operation of getview(). */
+	swap_view_roles();
+
 	ui_sb_msg("");
 	assert_success(vlua_run_string(vlua,
 				"print(vifm.tabs.get({ index = 1 }):getview({ pane = 2 }).cwd)"));
 	assert_string_equal("t1vr", ui_sb_last());
 
-	/* Accessing dead tab */
+	assert_success(vlua_run_string(vlua,
+				"print(vifm.tabs.get{}:getview{}.cwd)"));
+	assert_string_equal("t2vr", ui_sb_last());
+
+	/* Accessing dead tab should fail. */
 	ui_sb_msg("");
 	assert_success(vlua_run_string(vlua, "tab = vifm.tabs.get({ index = 2 })"));
 	tabs_close();
 	assert_failure(vlua_run_string(vlua, "tab:getview()"));
-	assert_true(ends_with(ui_sb_last(),
-				": Invalid VifmTab object (associated tab is dead)"));
+	assert_string_ends_with(": Invalid VifmTab object (associated tab is dead)",
+			ui_sb_last());
 }
 
 TEST(getview_pane_tabs)
@@ -306,8 +313,8 @@ TEST(getview_pane_tabs)
 	assert_success(vlua_run_string(vlua, "tab = vifm.tabs.get({ index = 2 })"));
 	tabs_close();
 	assert_failure(vlua_run_string(vlua, "tab:getview()"));
-	assert_true(ends_with(ui_sb_last(),
-				": Invalid VifmTab object (associated tab is dead)"));
+	assert_string_ends_with(": Invalid VifmTab object (associated tab is dead)",
+			ui_sb_last());
 }
 
 TEST(getlayout_global_tabs)
diff --git a/tests/lua/api_view.c b/tests/lua/api_view.c
index 215351c..1be7e9a 100644
--- a/tests/lua/api_view.c
+++ b/tests/lua/api_view.c
@@ -2,12 +2,17 @@
 
 #include <string.h> /* strdup() */
 
+#include "../../src/engine/keys.h"
 #include "../../src/int/file_magic.h"
 #include "../../src/lua/vlua.h"
+#include "../../src/modes/modes.h"
+#include "../../src/modes/visual.h"
+#include "../../src/modes/wk.h"
 #include "../../src/ui/statusbar.h"
 #include "../../src/ui/ui.h"
 #include "../../src/utils/dynarray.h"
 #include "../../src/utils/str.h"
+#include "../../src/filelist.h"
 
 #include <test-utils.h>
 
@@ -88,6 +93,139 @@ TEST(vifmview_entry)
 	assert_string_equal("nil", ui_sb_last());
 }
 
+TEST(vifmview_custom)
+{
+	ui_sb_msg("");
+	assert_success(vlua_run_string(vlua, "print(vifm.currview().custom)"));
+	assert_string_equal("nil", ui_sb_last());
+
+	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), "", "", NULL);
+	flist_custom_start(&lwin, "vifmview_custom");
+	flist_custom_add(&lwin, TEST_DATA_PATH "/existing-files/a");
+	assert_success(flist_custom_finish(&lwin, CV_REGULAR, /*allow_empty=*/0));
+
+	assert_success(vlua_run_string(vlua,
+				"print(vifm.currview().custom.title)"));
+	assert_string_equal("vifmview_custom", ui_sb_last());
+	assert_success(vlua_run_string(vlua,
+				"print(vifm.currview().custom.type)"));
+	assert_string_equal("custom", ui_sb_last());
+}
+
+TEST(vifmview_select)
+{
+	assert_false(lwin.dir_entry[0].selected);
+	assert_false(lwin.dir_entry[1].selected);
+
+	ui_sb_msg("");
+
+	assert_success(vlua_run_string(vlua, "print(vifm.currview():select({"
+	                                     "  indexes = { 0, 1.5, 10 }"
+	                                     "}))"));
+
+	assert_string_equal("0", ui_sb_last());
+	assert_false(lwin.dir_entry[0].selected);
+	assert_false(lwin.dir_entry[1].selected);
+
+	ui_sb_msg("");
+
+	assert_success(vlua_run_string(vlua, "print(vifm.currview():select({"
+	                                     "  indexes = { 2 }"
+	                                     "}))"));
+
+	assert_string_equal("1", ui_sb_last());
+	assert_false(lwin.dir_entry[0].selected);
+	assert_true(lwin.dir_entry[1].selected);
+
+	assert_success(vlua_run_string(vlua, "print(vifm.currview():select({"
+	                                     "  indexes = { 1, 2 }"
+	                                     "}))"));
+
+	assert_string_equal("1", ui_sb_last());
+	assert_true(lwin.dir_entry[0].selected);
+	assert_true(lwin.dir_entry[1].selected);
+}
+
+TEST(vifmview_select_in_visual_mode)
+{
+	modes_init();
+	opt_handlers_setup();
+
+	/* Does nothing in visual non-amend mode. */
+
+	modvis_enter(VS_NORMAL);
+
+	assert_false(lwin.dir_entry[0].selected);
+	assert_true(lwin.dir_entry[1].selected);
+
+	ui_sb_msg("");
+
+	assert_success(vlua_run_string(vlua, "print(vifm.currview():select({"
+	                                     "  indexes = { 1 }"
+	                                     "}))"));
+
+	assert_string_equal("0", ui_sb_last());
+	assert_false(lwin.dir_entry[0].selected);
+	assert_true(lwin.dir_entry[1].selected);
+
+	/* Selects in visual amend mode. */
+
+	modvis_enter(VS_AMEND);
+
+	assert_false(lwin.dir_entry[0].selected);
+	assert_true(lwin.dir_entry[1].selected);
+
+	ui_sb_msg("");
+
+	assert_success(vlua_run_string(vlua, "print(vifm.currview():select({"
+	                                     "  indexes = { 1 }"
+	                                     "}))"));
+
+	assert_string_equal("1", ui_sb_last());
+	assert_true(lwin.dir_entry[0].selected);
+	assert_true(lwin.dir_entry[1].selected);
+
+	modvis_leave(/*save_msg=*/0, /*goto_top=*/1, /*clear_selection=*/1);
+
+	(void)vle_keys_exec_timed_out(WK_C_c);
+	vle_keys_reset();
+	opt_handlers_teardown();
+}
+
+TEST(vifmview_unselect)
+{
+	lwin.dir_entry[0].selected = 1;
+	lwin.dir_entry[1].selected = 1;
+
+	ui_sb_msg("");
+
+	assert_success(vlua_run_string(vlua, "print(vifm.currview():unselect({"
+	                                     "  indexes = { 0, 1.5, 10 }"
+	                                     "}))"));
+
+	assert_string_equal("0", ui_sb_last());
+	assert_true(lwin.dir_entry[0].selected);
+	assert_true(lwin.dir_entry[1].selected);
+
+	ui_sb_msg("");
+
+	assert_success(vlua_run_string(vlua, "print(vifm.currview():unselect({"
+	                                     "  indexes = { 2 }"
+	                                     "}))"));
+
+	assert_string_equal("1", ui_sb_last());
+	assert_true(lwin.dir_entry[0].selected);
+	assert_false(lwin.dir_entry[1].selected);
+
+	assert_success(vlua_run_string(vlua, "print(vifm.currview():unselect({"
+	                                     "  indexes = { 1, 2 }"
+	                                     "}))"));
+
+	assert_string_equal("1", ui_sb_last());
+	assert_false(lwin.dir_entry[0].selected);
+	assert_false(lwin.dir_entry[1].selected);
+}
+
 TEST(vifmview_entry_mimetype_unavailable, IF(has_no_mime_type_detection))
 {
 	ui_sb_msg("");
diff --git a/tests/lua/api_viewcolumns.c b/tests/lua/api_viewcolumns.c
index cc63698..457708b 100644
--- a/tests/lua/api_viewcolumns.c
+++ b/tests/lua/api_viewcolumns.c
@@ -7,7 +7,6 @@
 #include "../../src/ui/fileview.h"
 #include "../../src/ui/statusbar.h"
 #include "../../src/ui/ui.h"
-#include "../../src/utils/str.h"
 #include "../../src/opt_handlers.h"
 
 #include <test-utils.h>
@@ -48,13 +47,13 @@ TEST(bad_args)
 	assert_failure(vlua_run_string(vlua,
 				"print(vifm.addcolumntype{ name = nil,"
 				                         " handler = nil })"));
-	assert_true(ends_with(ui_sb_last(), ": `name` key is mandatory"));
+	assert_string_ends_with(": `name` key is mandatory", ui_sb_last());
 
 	ui_sb_msg("");
 	assert_failure(vlua_run_string(vlua,
 				"print(vifm.addcolumntype{ name = 'NAME',"
 				                         " handler = nil })"));
-	assert_true(ends_with(ui_sb_last(), ": `handler` key is mandatory"));
+	assert_string_ends_with(": `handler` key is mandatory", ui_sb_last());
 }
 
 TEST(bad_name)
@@ -65,21 +64,22 @@ TEST(bad_name)
 	assert_failure(vlua_run_string(vlua,
 				"print(vifm.addcolumntype{ name = '',"
 				                         " handler = handler })"));
-	assert_true(ends_with(ui_sb_last(), ": View column name can't be empty"));
+	assert_string_ends_with(": View column name can't be empty", ui_sb_last());
 
 	ui_sb_msg("");
 	assert_failure(vlua_run_string(vlua,
 				"print(vifm.addcolumntype{ name = 'name',"
 				                         " handler = handler })"));
-	assert_true(ends_with(ui_sb_last(),
-				": View column name must not start with a lower case Latin letter"));
+	assert_string_ends_with(
+			": View column name must not start with a lower case Latin letter",
+			ui_sb_last());
 
 	ui_sb_msg("");
 	assert_failure(vlua_run_string(vlua,
 				"print(vifm.addcolumntype{ name = 'A-A',"
 				                         " handler = handler })"));
-	assert_true(ends_with(ui_sb_last(),
-				": View column name must not contain non-Latin characters"));
+	assert_string_ends_with(
+			": View column name must not contain non-Latin characters", ui_sb_last());
 }
 
 TEST(column_is_registered)
@@ -108,8 +108,8 @@ TEST(duplicate_name)
 	assert_failure(vlua_run_string(vlua,
 				"print(vifm.addcolumntype{ name = 'Test',"
 				                         " handler = handler })"));
-	assert_true(ends_with(ui_sb_last(),
-				": View column with such name already exists: Test"));
+	assert_string_ends_with(": View column with such name already exists: Test",
+			ui_sb_last());
 }
 
 TEST(columns_are_used)
@@ -153,8 +153,6 @@ TEST(columns_are_used)
 	assert_string_equal("     ERROR   NOVALUE   NOVALUE    name10", print_buffer);
 
 	opt_handlers_teardown();
-	columns_free(lwin.columns);
-	lwin.columns = NULL;
 	curr_stats.vlua = NULL;
 }
 
@@ -200,8 +198,6 @@ TEST(symlinks, IF(not_windows))
 	assert_string_equal("link -> something                       ", print_buffer);
 
 	opt_handlers_teardown();
-	columns_free(lwin.columns);
-	lwin.columns = NULL;
 	curr_stats.vlua = NULL;
 
 	remove_file(SANDBOX_PATH "/symlink");
diff --git a/tests/lua/plugins.c b/tests/lua/plugins.c
index 7c419ed..20c8749 100644
--- a/tests/lua/plugins.c
+++ b/tests/lua/plugins.c
@@ -1,9 +1,11 @@
 #include <stic.h>
 
 #include "../../src/cfg/config.h"
+#include "../../src/compat/fs_limits.h"
 #include "../../src/lua/vlua.h"
 #include "../../src/ui/statusbar.h"
 #include "../../src/utils/str.h"
+#include "../../src/utils/string_array.h"
 #include "../../src/plugins.h"
 
 #include <test-utils.h>
@@ -22,6 +24,13 @@ SETUP()
 
 	vlua = vlua_init();
 	plugs = plugs_create(vlua);
+
+	char plug_path[PATH_MAX + 1];
+	make_abs_path(plug_path, sizeof(plug_path), SANDBOX_PATH, "plugins/plug",
+			NULL);
+
+	update_string(&plug_dummy.name, "plug");
+	update_string(&plug_dummy.path, plug_path);
 }
 
 TEARDOWN()
@@ -34,6 +43,8 @@ TEARDOWN()
 	remove_dir(SANDBOX_PATH "/plugins/plug");
 	remove_dir(SANDBOX_PATH "/plugins");
 
+	update_string(&plug_dummy.name, NULL);
+	update_string(&plug_dummy.path, NULL);
 	update_string(&plug_dummy.log, NULL);
 	plug_dummy.log_len = 0;
 }
@@ -43,7 +54,7 @@ TEST(good_plugin_loaded)
 	make_file(SANDBOX_PATH "/plugins/plug/init.lua", "return {}");
 
 	ui_sb_msg("");
-	assert_success(vlua_load_plugin(vlua, "plug", &plug_dummy));
+	assert_success(vlua_load_plugin(vlua, &plug_dummy));
 	assert_string_equal("", ui_sb_last());
 
 	remove_file(SANDBOX_PATH "/plugins/plug/init.lua");
@@ -54,7 +65,7 @@ TEST(bad_return_value)
 	make_file(SANDBOX_PATH "/plugins/plug/init.lua", "return 123");
 
 	ui_sb_msg("");
-	assert_failure(vlua_load_plugin(vlua, "plug", &plug_dummy));
+	assert_failure(vlua_load_plugin(vlua, &plug_dummy));
 	assert_string_equal("Failed to load 'plug' plugin: it didn't return a table",
 			ui_sb_last());
 
@@ -66,8 +77,8 @@ TEST(syntax_error)
 	make_file(SANDBOX_PATH "/plugins/plug/init.lua", "-+");
 
 	ui_sb_msg("");
-	assert_failure(vlua_load_plugin(vlua, "plug", &plug_dummy));
-	assert_true(starts_with_lit(ui_sb_last(), "Failed to load 'plug' plugin: "));
+	assert_failure(vlua_load_plugin(vlua, &plug_dummy));
+	assert_string_starts_with("Failed to load 'plug' plugin: ", ui_sb_last());
 
 	remove_file(SANDBOX_PATH "/plugins/plug/init.lua");
 }
@@ -77,8 +88,8 @@ TEST(runtime_error)
 	make_file(SANDBOX_PATH "/plugins/plug/init.lua", "badcall()");
 
 	ui_sb_msg("");
-	assert_failure(vlua_load_plugin(vlua, "plug", &plug_dummy));
-	assert_true(starts_with_lit(ui_sb_last(), "Failed to start 'plug' plugin: "));
+	assert_failure(vlua_load_plugin(vlua, &plug_dummy));
+	assert_string_starts_with("Failed to start 'plug' plugin: ", ui_sb_last());
 
 	remove_file(SANDBOX_PATH "/plugins/plug/init.lua");
 }
@@ -89,7 +100,7 @@ TEST(hidden_dir_is_ignored)
 	create_dir(SANDBOX_PATH "/plugins/.git");
 
 	ui_sb_msg("");
-	plugs_load(plugs, cfg.config_dir);
+	load_plugins(plugs, cfg.config_dir);
 	assert_string_equal("", ui_sb_last());
 
 	remove_dir(SANDBOX_PATH "/plugins/.git");
@@ -103,7 +114,7 @@ TEST(multiple_plugins_loaded)
 	make_file(SANDBOX_PATH "/plugins/plug2/init.lua", "return {}");
 
 	ui_sb_msg("");
-	plugs_load(plugs, cfg.config_dir);
+	load_plugins(plugs, cfg.config_dir);
 	assert_string_equal("", ui_sb_last());
 
 	assert_success(vlua_run_string(vlua,
@@ -122,7 +133,7 @@ TEST(can_load_plugins_only_once)
 	assert_false(plugs_loaded(plugs));
 
 	ui_sb_msg("");
-	plugs_load(plugs, cfg.config_dir);
+	load_plugins(plugs, cfg.config_dir);
 	assert_string_equal("", ui_sb_last());
 
 	assert_true(plugs_loaded(plugs));
@@ -131,7 +142,7 @@ TEST(can_load_plugins_only_once)
 	assert_true(plugs_get(plugs, 0, &plug));
 	assert_false(plugs_get(plugs, 1, &plug));
 
-	plugs_load(plugs, cfg.config_dir);
+	load_plugins(plugs, cfg.config_dir);
 	assert_string_equal("", ui_sb_last());
 	assert_true(plugs_loaded(plugs));
 
@@ -143,7 +154,7 @@ TEST(can_load_plugins_only_once)
 
 TEST(loading_missing_plugin_fails)
 {
-	assert_failure(vlua_load_plugin(vlua, "plug", &plug_dummy));
+	assert_failure(vlua_load_plugin(vlua, &plug_dummy));
 }
 
 TEST(plugin_statuses_are_correct)
@@ -152,7 +163,7 @@ TEST(plugin_statuses_are_correct)
 	make_file(SANDBOX_PATH "/plugins/plug/init.lua", "return {}");
 	make_file(SANDBOX_PATH "/plugins/plug2/init.lua", "return");
 
-	plugs_load(plugs, cfg.config_dir);
+	load_plugins(plugs, cfg.config_dir);
 
 	const plug_t *plug;
 	PluginLoadStatus status;
@@ -178,7 +189,7 @@ TEST(plugins_can_be_blacklisted)
 	plugs_blacklist(plugs, "plug2");
 
 	ui_sb_msg("");
-	plugs_load(plugs, cfg.config_dir);
+	load_plugins(plugs, cfg.config_dir);
 	assert_string_equal("", ui_sb_last());
 
 	assert_success(vlua_run_string(vlua,
@@ -201,7 +212,7 @@ TEST(plugins_can_be_whitelisted)
 	plugs_blacklist(plugs, "plug2");
 
 	ui_sb_msg("");
-	plugs_load(plugs, cfg.config_dir);
+	load_plugins(plugs, cfg.config_dir);
 	assert_string_equal("", ui_sb_last());
 
 	assert_success(vlua_run_string(vlua,
@@ -220,7 +231,7 @@ TEST(plugin_metadata)
 			"return { name = vifm.plugin.name }");
 
 	ui_sb_msg("");
-	plugs_load(plugs, cfg.config_dir);
+	load_plugins(plugs, cfg.config_dir);
 	assert_success(vlua_run_string(vlua, "print(vifm.plugins.all.plug.name)"));
 	assert_string_equal("plug", ui_sb_last());
 
@@ -232,13 +243,16 @@ TEST(good_plugin_module)
 	make_file(SANDBOX_PATH "/plugins/plug/init.lua",
 			"return vifm.plugin.require('sub')");
 	make_file(SANDBOX_PATH "/plugins/plug/sub.lua",
-			"return { source = 'sub' }");
+			"return vifm.plugin.require('subsub')");
+	make_file(SANDBOX_PATH "/plugins/plug/subsub.lua",
+			"return { source = 'subsub' }");
 
 	ui_sb_msg("");
-	plugs_load(plugs, cfg.config_dir);
+	load_plugins(plugs, cfg.config_dir);
 	assert_success(vlua_run_string(vlua, "print(vifm.plugins.all.plug.source)"));
-	assert_string_equal("sub", ui_sb_last());
+	assert_string_equal("subsub", ui_sb_last());
 
+	remove_file(SANDBOX_PATH "/plugins/plug/subsub.lua");
 	remove_file(SANDBOX_PATH "/plugins/plug/sub.lua");
 	remove_file(SANDBOX_PATH "/plugins/plug/init.lua");
 }
@@ -249,7 +263,7 @@ TEST(missing_plugin_module)
 			"return vifm.plugin.require('sub')");
 
 	ui_sb_msg("");
-	plugs_load(plugs, cfg.config_dir);
+	load_plugins(plugs, cfg.config_dir);
 	assert_success(vlua_run_string(vlua, "print(vifm.plugins.all.plug)"));
 	assert_string_equal("nil", ui_sb_last());
 
@@ -264,7 +278,7 @@ TEST(plugins_can_add_handler)
 			"return {}");
 
 	ui_sb_msg("");
-	plugs_load(plugs, cfg.config_dir);
+	load_plugins(plugs, cfg.config_dir);
 	assert_string_equal("", ui_sb_last());
 
 	assert_true(vlua_handler_present(vlua, "#plug#handler"));
@@ -277,7 +291,7 @@ TEST(can_not_load_same_plugin_twice, IF(not_windows))
 	make_file(SANDBOX_PATH "/plugins/plug/init.lua", "return {}");
 	assert_success(make_symlink("plug", SANDBOX_PATH "/plugins/plug2"));
 
-	plugs_load(plugs, cfg.config_dir);
+	load_plugins(plugs, cfg.config_dir);
 
 	const plug_t *plug1, *plug2, *plug3;
 	assert_true(plugs_get(plugs, 0, &plug1));
@@ -290,8 +304,8 @@ TEST(can_not_load_same_plugin_twice, IF(not_windows))
 	            (status2 == PLS_SUCCESS && status1 == PLS_SKIPPED));
 
 	const plug_t *skipped = (status1 == PLS_SKIPPED ? plug1 : plug2);
-	assert_true(starts_with_lit(skipped->log,
-				"[vifm][error]: skipped as a duplicate of"));
+	assert_string_starts_with("[vifm][error]: skipped as a duplicate of",
+			skipped->log);
 
 	remove_file(SANDBOX_PATH "/plugins/plug/init.lua");
 	remove_file(SANDBOX_PATH "/plugins/plug2");
@@ -303,7 +317,7 @@ TEST(print_outputs_to_plugin_log)
 			"print('arg1', 'arg2'); return {}");
 
 	ui_sb_msg("");
-	assert_success(vlua_load_plugin(vlua, "plug", &plug_dummy));
+	assert_success(vlua_load_plugin(vlua, &plug_dummy));
 	assert_string_equal("", ui_sb_last());
 	assert_string_equal("arg1\targ2", plug_dummy.log);
 
@@ -316,11 +330,94 @@ TEST(print_without_arguments)
 			"print('first line'); print(); print('third line'); return {}");
 
 	update_string(&plug_dummy.log, NULL);
-	assert_success(vlua_load_plugin(vlua, "plug", &plug_dummy));
+	assert_success(vlua_load_plugin(vlua, &plug_dummy));
 	assert_string_equal("first line\n\nthird line", plug_dummy.log);
 
 	remove_file(SANDBOX_PATH "/plugins/plug/init.lua");
 }
 
+TEST(can_not_load_plugins_with_the_same_name)
+{
+	create_dir(SANDBOX_PATH "/plugins2");
+	create_dir(SANDBOX_PATH "/plugins2/plug");
+
+	make_file(SANDBOX_PATH "/plugins/plug/init.lua",
+			"return { src = 'plugins' }");
+	make_file(SANDBOX_PATH "/plugins2/plug/init.lua",
+			"return { src = 'plugins2' }");
+
+	strlist_t plugins_dirs = { };
+	plugins_dirs.nitems = add_to_string_array(&plugins_dirs.items,
+			plugins_dirs.nitems, SANDBOX_PATH "/plugins2");
+	plugins_dirs.nitems = add_to_string_array(&plugins_dirs.items,
+			plugins_dirs.nitems, SANDBOX_PATH "/plugins");
+
+	plugs_load(plugs, plugins_dirs);
+
+	free_string_array(plugins_dirs.items, plugins_dirs.nitems);
+
+	assert_success(vlua_run_string(vlua, "print(vifm.plugins.all.plug.src)"));
+	assert_string_equal("plugins2", ui_sb_last());
+
+	const plug_t *plug1, *plug2;
+	assert_true(plugs_get(plugs, 0, &plug1));
+	PluginLoadStatus status1 = plug1->status;
+	assert_true(plugs_get(plugs, 1, &plug2));
+	PluginLoadStatus status2 = plug2->status;
+
+	assert_int_equal(PLS_SUCCESS, status1);
+	assert_int_equal(PLS_SKIPPED, status2);
+
+	remove_file(SANDBOX_PATH "/plugins/plug/init.lua");
+	remove_file(SANDBOX_PATH "/plugins2/plug/init.lua");
+	remove_dir(SANDBOX_PATH "/plugins2/plug");
+	remove_dir(SANDBOX_PATH "/plugins2");
+}
+
+TEST(plugin_dir_does_not_need_to_exist)
+{
+	load_plugins(plugs, SANDBOX_PATH "/no-such-path");
+}
+
+TEST(global_table_is_mocked)
+{
+	create_dir(SANDBOX_PATH "/plugins/plug2");
+	make_file(SANDBOX_PATH "/plugins/plug/init.lua",
+			"_G.data = 'plug1' "
+			"return {}");
+	make_file(SANDBOX_PATH "/plugins/plug2/init.lua",
+			"function _G.func()"
+			"  return data "
+			"end "
+			"return {}");
+
+	ui_sb_msg("");
+	load_plugins(plugs, cfg.config_dir);
+	assert_string_equal("", ui_sb_last());
+
+	assert_failure(vlua_run_string(vlua, "print(func())"));
+	assert_string_ends_with(": attempt to call a nil value (global 'func')",
+			ui_sb_last());
+
+	remove_file(SANDBOX_PATH "/plugins/plug/init.lua");
+	remove_file(SANDBOX_PATH "/plugins/plug2/init.lua");
+	remove_dir(SANDBOX_PATH "/plugins/plug2");
+}
+
+TEST(metatables_are_protected)
+{
+	make_file(SANDBOX_PATH "/plugins/plug/init.lua", "return { _G = _G }");
+
+	ui_sb_msg("");
+	load_plugins(plugs, cfg.config_dir);
+	assert_string_equal("", ui_sb_last());
+
+	assert_success(vlua_run_string(vlua,
+				"print(getmetatable(vifm.plugins.all.plug._G))"));
+	assert_string_equal("false", ui_sb_last());
+
+	remove_file(SANDBOX_PATH "/plugins/plug/init.lua");
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 : */
diff --git a/tests/lua/suite.c b/tests/lua/suite.c
index d91d84c..46ebeb5 100644
--- a/tests/lua/suite.c
+++ b/tests/lua/suite.c
@@ -19,7 +19,7 @@ SETUP_ONCE()
 	 * nothing will change the path before we try to save it. */
 	saved_cwd = save_cwd();
 
-	bg_init();
+	assert_success(bg_init());
 	tabs_init();
 	setup_signals();
 }
diff --git a/tests/misc/menus_bmarks.c b/tests/menus/bmarks.c
similarity index 73%
rename from tests/misc/menus_bmarks.c
rename to tests/menus/bmarks.c
index e72ad75..ef3b293 100644
--- a/tests/misc/menus_bmarks.c
+++ b/tests/menus/bmarks.c
@@ -37,8 +37,8 @@ SETUP_ONCE()
 SETUP()
 {
 	conf_setup();
-	init_modes();
-	init_commands();
+	modes_init();
+	cmds_init();
 	bmarks_clear();
 
 	curr_view = &lwin;
@@ -64,9 +64,9 @@ TEARDOWN()
 
 TEST(list_of_bmarks_is_filtered)
 {
-	assert_success(exec_commands("bmark! /a taga", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmark! /b tagb", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmarks taga", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /a taga", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /b tagb", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmarks taga", &lwin, CIT_COMMAND));
 
 	assert_int_equal(2, count_bmarks());
 	assert_int_equal(1, menu_get_current()->len);
@@ -77,8 +77,8 @@ TEST(enter_navigates_to_selected_bmark)
 {
 	char cmd[PATH_MAX + 1];
 	snprintf(cmd, sizeof(cmd), "bmark! '%s/read' taga", test_data);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmarks", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmarks", &lwin, CIT_COMMAND));
 
 	lwin.curr_dir[0] = '\0';
 	(void)vle_keys_exec(WK_CR);
@@ -90,8 +90,8 @@ TEST(gf_navigates_to_selected_bmark)
 {
 	char cmd[PATH_MAX + 1];
 	snprintf(cmd, sizeof(cmd), "bmark! '%s/' taga", test_data);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmarks", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmarks", &lwin, CIT_COMMAND));
 
 	lwin.curr_dir[0] = '\0';
 	(void)vle_keys_exec(WK_g WK_f);
@@ -104,8 +104,8 @@ TEST(e_opens_selected_bmark)
 	char buf[PATH_MAX + 1];
 
 	snprintf(buf, sizeof(buf), "bmark! '%s/read' taga", test_data);
-	assert_success(exec_commands(buf, &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmarks", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(buf, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmarks", &lwin, CIT_COMMAND));
 
 #ifndef _WIN32
 	replace_string(&cfg.shell, "/bin/sh");
@@ -138,9 +138,9 @@ TEST(e_opens_selected_bmark)
 
 TEST(bmark_is_deleted)
 {
-	assert_success(exec_commands("bmark! /a taga", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmark! /b tagb", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmarks", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /a taga", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /b tagb", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmarks", &lwin, CIT_COMMAND));
 
 	assert_int_equal(2, count_bmarks());
 	(void)vle_keys_exec(WK_d WK_d);
@@ -152,8 +152,8 @@ TEST(unhandled_key_is_ignored)
 {
 	char cmd[PATH_MAX + 1];
 	snprintf(cmd, sizeof(cmd), "bmark! '%s/read' taga", test_data);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmarks", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmarks", &lwin, CIT_COMMAND));
 
 	(void)vle_keys_exec(WK_x);
 	(void)vle_keys_exec(WK_ESC);
@@ -164,25 +164,25 @@ TEST(bmgo_navigates_to_single_match)
 	lwin.curr_dir[0] = '\0';
 	char cmd[PATH_MAX + 1];
 	snprintf(cmd, sizeof(cmd), "bmark! '%s/read' taga", test_data);
-	assert_success(exec_commands(cmd, &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmgo", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch(cmd, &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmgo", &lwin, CIT_COMMAND));
 	assert_true(paths_are_equal(lwin.curr_dir, test_data));
 }
 
 TEST(sorting_updates_associated_data)
 {
-	assert_success(exec_commands("bmark! /c tagb", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmark! /b tagb", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmark! /a taga", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("bmarks", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /c tagb", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /b tagb", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! /a taga", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmarks", &lwin, CIT_COMMAND));
 
 	assert_int_equal(3, menu_get_current()->len);
 	assert_string_equal("/a", menu_get_current()->data[0]);
 	assert_string_equal("/b", menu_get_current()->data[1]);
 	assert_string_equal("/c", menu_get_current()->data[2]);
-	assert_true(starts_with_lit(menu_get_current()->items[0], "/a"));
-	assert_true(starts_with_lit(menu_get_current()->items[1], "/b"));
-	assert_true(starts_with_lit(menu_get_current()->items[2], "/c"));
+	assert_string_starts_with("/a", menu_get_current()->items[0]);
+	assert_string_starts_with("/b", menu_get_current()->items[1]);
+	assert_string_starts_with("/c", menu_get_current()->items[2]);
 	(void)vle_keys_exec(WK_ESC);
 }
 
diff --git a/tests/misc/menus_dirhistory.c b/tests/menus/dirhistory.c
similarity index 100%
rename from tests/misc/menus_dirhistory.c
rename to tests/menus/dirhistory.c
diff --git a/tests/misc/menus_filetypes.c b/tests/menus/filetypes.c
similarity index 86%
rename from tests/misc/menus_filetypes.c
rename to tests/menus/filetypes.c
index c97e1e9..cea1d24 100644
--- a/tests/misc/menus_filetypes.c
+++ b/tests/menus/filetypes.c
@@ -35,8 +35,8 @@ SETUP_ONCE()
 SETUP()
 {
 	conf_setup();
-	init_modes();
-	init_commands();
+	modes_init();
+	cmds_init();
 
 	curr_view = &lwin;
 	other_view = &rwin;
@@ -64,7 +64,7 @@ TEARDOWN()
 
 TEST(opening_a_directory_works)
 {
-	assert_success(exec_commands("file", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("file", &lwin, CIT_COMMAND));
 
 	(void)vle_keys_exec(WK_CR);
 
@@ -79,13 +79,13 @@ TEST(opening_a_directory_works)
 TEST(no_menu_for_fake_entry)
 {
 	get_current_entry(&lwin)->name[0] = '\0';
-	assert_success(exec_commands("file", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("file", &lwin, CIT_COMMAND));
 	assert_true(vle_mode_is(NORMAL_MODE));
 }
 
 TEST(c_key_is_handled)
 {
-	assert_success(exec_commands("file", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("file", &lwin, CIT_COMMAND));
 
 	(void)vle_keys_exec(WK_c);
 	assert_true(vle_mode_is(CMDLINE_MODE));
@@ -97,7 +97,7 @@ TEST(c_key_is_handled)
 
 TEST(unknown_key_is_ignored)
 {
-	assert_success(exec_commands("file", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("file", &lwin, CIT_COMMAND));
 
 	(void)vle_keys_exec(WK_t);
 	(void)vle_keys_exec(WK_CR);
@@ -118,7 +118,7 @@ TEST(pseudo_entry_is_always_present_for_directories)
 
 	ft_set_programs(ms, "abc-run %c", 0, 0);
 
-	assert_success(exec_commands("filetype bla-dir/", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filetype bla-dir/", &lwin, CIT_COMMAND));
 
 	assert_int_equal(2, menu_get_current()->len);
 	assert_string_equal("[present] [Enter directory] " VIFM_PSEUDO_CMD,
@@ -131,7 +131,7 @@ TEST(pseudo_entry_is_always_present_for_directories)
 
 TEST(no_menu_if_no_handlers)
 {
-	assert_failure(exec_commands("filetype bla-file", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("filetype bla-file", &lwin, CIT_COMMAND));
 }
 
 TEST(filetypes_menu)
@@ -142,7 +142,7 @@ TEST(filetypes_menu)
 
 	ft_set_programs(ms, "abc-run %c", 0, 0);
 
-	assert_success(exec_commands("filetype b", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filetype b", &lwin, CIT_COMMAND));
 
 	assert_int_equal(1, menu_get_current()->len);
 	assert_string_equal("[present] abc-run %c", menu_get_current()->items[0]);
@@ -158,7 +158,7 @@ TEST(fileviewers_menu)
 
 	ft_set_viewers(ms, "abc-view %c");
 
-	assert_success(exec_commands("fileviewer c", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("fileviewer c", &lwin, CIT_COMMAND));
 
 	assert_int_equal(1, menu_get_current()->len);
 	assert_string_equal("[present] abc-view %c", menu_get_current()->items[0]);
diff --git a/tests/misc/menus_find.c b/tests/menus/find.c
similarity index 78%
rename from tests/misc/menus_find.c
rename to tests/menus/find.c
index e31022a..83d966f 100644
--- a/tests/misc/menus_find.c
+++ b/tests/menus/find.c
@@ -31,7 +31,7 @@ SETUP_ONCE()
 
 SETUP()
 {
-	init_modes();
+	modes_init();
 
 	view_setup(&lwin);
 	view_setup(&rwin);
@@ -41,7 +41,7 @@ SETUP()
 
 	opt_handlers_setup();
 
-	init_commands();
+	cmds_init();
 
 	curr_stats.load_stage = -1;
 }
@@ -67,22 +67,22 @@ TEST(find_command, IF(not_windows))
 	assert_success(chdir(TEST_DATA_PATH));
 	strcpy(lwin.curr_dir, test_data);
 
-	assert_success(exec_commands("set findprg='find %s %a %u'", &lwin,
+	assert_success(cmds_dispatch("set findprg='find %s %a %u'", &lwin,
 				CIT_COMMAND));
 
 	/* Nothing to repeat. */
 	cmds_drop_state();
-	assert_failure(exec_commands("find", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("find", &lwin, CIT_COMMAND));
 
-	assert_success(exec_commands("find a", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("find a", &lwin, CIT_COMMAND));
 	assert_int_equal(3, lwin.list_rows);
 	assert_string_equal("Find a", lwin.custom.title);
 
-	assert_success(exec_commands("find . -name aaa", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("find . -name aaa", &lwin, CIT_COMMAND));
 	assert_int_equal(1, lwin.list_rows);
 	assert_string_equal("Find . -name aaa", lwin.custom.title);
 
-	assert_success(exec_commands("find -name '*.vifm'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("find -name '*.vifm'", &lwin, CIT_COMMAND));
 	assert_int_equal(11, lwin.list_rows);
 	assert_string_equal("Find -name '*.vifm'", lwin.custom.title);
 
@@ -91,18 +91,18 @@ TEST(find_command, IF(not_windows))
 
 	/* Repeat last search. */
 	strcpy(lwin.curr_dir, test_data);
-	assert_success(exec_commands("find", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("find", &lwin, CIT_COMMAND));
 	assert_int_equal(11, lwin.list_rows);
 }
 
 TEST(enter_navigates_to_found_file, IF(not_windows))
 {
-	assert_success(exec_commands("set findprg='find %s %a'", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set findprg='find %s %a'", &lwin, CIT_COMMAND));
 
 	assert_success(chdir(TEST_DATA_PATH));
 	strcpy(lwin.curr_dir, test_data);
 
-	assert_success(exec_commands("find dir1", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("find dir1", &lwin, CIT_COMMAND));
 
 	char dst[PATH_MAX + 1];
 	snprintf(dst, sizeof(dst), "%s/tree", test_data);
@@ -114,13 +114,13 @@ TEST(enter_navigates_to_found_file, IF(not_windows))
 
 TEST(p_macro_works, IF(not_windows))
 {
-	assert_success(exec_commands("set findprg='find %s -name %p'", &lwin,
+	assert_success(cmds_dispatch("set findprg='find %s -name %p'", &lwin,
 				CIT_COMMAND));
 
 	assert_success(chdir(TEST_DATA_PATH));
 	strcpy(lwin.curr_dir, test_data);
 
-	assert_failure(exec_commands("find a$NO_SUCH_VAR", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("find a$NO_SUCH_VAR", &lwin, CIT_COMMAND));
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/tests/menus/generic.c b/tests/menus/generic.c
new file mode 100644
index 0000000..4cbb386
--- /dev/null
+++ b/tests/menus/generic.c
@@ -0,0 +1,51 @@
+#include <stic.h>
+
+#include <unistd.h> /* chdir() */
+
+#include <stdlib.h> /* remove() */
+#include <string.h> /* strcpy() strdup() */
+
+#include <test-utils.h>
+
+#include "../../src/compat/fs_limits.h"
+#include "../../src/menus/menus.h"
+#include "../../src/ui/ui.h"
+#include "../../src/utils/fs.h"
+
+/* This tests implementation of menus without activating the mode. */
+
+static menu_data_t m;
+
+SETUP()
+{
+	menus_init_data(&m, &lwin, strdup("test"), strdup("No matches"));
+}
+
+TEARDOWN()
+{
+	menus_reset_data(&m);
+}
+
+TEST(can_navigate_to_broken_symlink, IF(not_windows))
+{
+	char *saved_cwd;
+	char buf[PATH_MAX + 1];
+
+	strcpy(lwin.curr_dir, ".");
+
+	saved_cwd = save_cwd();
+	assert_success(chdir(SANDBOX_PATH));
+
+	assert_success(make_symlink("/wrong/path", "broken-link"));
+
+	make_abs_path(buf, sizeof(buf), SANDBOX_PATH , "broken-link:", saved_cwd);
+	/* Were trying to open broken link, which will fail, but the parsing part
+	 * should succeed. */
+	restore_cwd(saved_cwd);
+	assert_success(menus_goto_file(&m, &lwin, buf, 1));
+
+	assert_success(remove(SANDBOX_PATH "/broken-link"));
+}
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/misc/menus_history.c b/tests/menus/history.c
similarity index 72%
rename from tests/misc/menus_history.c
rename to tests/menus/history.c
index dbb3b1a..ca81038 100644
--- a/tests/misc/menus_history.c
+++ b/tests/menus/history.c
@@ -13,6 +13,7 @@
 #include "../../src/ui/statusbar.h"
 #include "../../src/ui/ui.h"
 #include "../../src/cmd_core.h"
+#include "../../src/filelist.h"
 #include "../../src/status.h"
 
 static line_stats_t *stats;
@@ -24,20 +25,13 @@ SETUP_ONCE()
 
 SETUP()
 {
-	enum { HISTORY_SIZE = 10 };
-
-	init_modes();
-	init_commands();
+	modes_init();
+	cmds_init();
 
 	curr_view = &lwin;
 	view_setup(&lwin);
 
-	/* Emulate proper history initialization (must happen after view
-	 * initialization). */
-	cfg_resize_histories(HISTORY_SIZE);
-	cfg_resize_histories(0);
-
-	cfg_resize_histories(HISTORY_SIZE);
+	histories_init(/*size=*/10);
 
 	curr_stats.load_stage = -1;
 }
@@ -185,5 +179,64 @@ TEST(unknown_key_is_ignored)
 	assert_true(vle_mode_is(MENU_MODE));
 }
 
+TEST(fsearch_hist_sets_search_direction_on_pick)
+{
+	hists_search_save("a");
+	assert_success(show_fsearchhistory_menu(&lwin));
+
+	(void)vle_keys_exec(WK_CR);
+	assert_false(curr_stats.last_search_backward);
+}
+
+TEST(bsearch_hist_sets_search_direction_on_pick)
+{
+	hists_search_save("a");
+	assert_success(show_bsearchhistory_menu(&lwin));
+
+	(void)vle_keys_exec(WK_CR);
+	assert_true(curr_stats.last_search_backward);
+}
+
+TEST(fsearch_hist_sets_search_direction_on_editing)
+{
+	hists_search_save("a");
+	assert_success(show_fsearchhistory_menu(&lwin));
+
+	(void)vle_keys_exec(WK_c);
+	assert_false(curr_stats.last_search_backward);
+	(void)vle_keys_exec_timed_out(WK_ESC);
+}
+
+TEST(bsearch_hist_sets_search_direction_on_editing)
+{
+	hists_search_save("a");
+	assert_success(show_bsearchhistory_menu(&lwin));
+
+	(void)vle_keys_exec(WK_c);
+	assert_true(curr_stats.last_search_backward);
+	(void)vle_keys_exec_timed_out(WK_ESC);
+}
+
+TEST(editing_search_performs_interactive_search)
+{
+	cfg.inc_search = 1;
+	opt_handlers_setup();
+
+	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), TEST_DATA_PATH, "", NULL);
+	populate_dir_list(&lwin, 0);
+	assert_int_equal(0, lwin.list_pos);
+
+	hists_search_save("var");
+	assert_success(show_fsearchhistory_menu(&lwin));
+
+	(void)vle_keys_exec(WK_c);
+	assert_int_equal(9, lwin.list_pos);
+	(void)vle_keys_exec_timed_out(WK_ESC);
+	assert_int_equal(0, lwin.list_pos);
+
+	opt_handlers_teardown();
+	cfg.inc_search = 0;
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 : */
diff --git a/tests/misc/menus_jobs.c b/tests/menus/jobs.c
similarity index 77%
rename from tests/misc/menus_jobs.c
rename to tests/menus/jobs.c
index 8d98729..bb1e6ea 100644
--- a/tests/misc/menus_jobs.c
+++ b/tests/menus/jobs.c
@@ -35,27 +35,28 @@ TEARDOWN_ONCE()
 
 SETUP()
 {
-	conf_setup();
-	init_modes();
-	init_commands();
-
 	curr_view = &lwin;
 	other_view = &rwin;
 	view_setup(&lwin);
 
+	/* The redraw code updates 'columns' and 'lines'. */
+	opt_handlers_setup();
+	modes_init();
+	cmds_init();
+
 	curr_stats.load_stage = -1;
 
-	assert_success(bg_execute("", "", 0, 0, &task, (void *)locks));
+	assert_success(bg_execute("job", "", 0, 0, &task, (void *)locks));
 	wait_until_locked(&locks[0]);
 
-	assert_success(exec_commands("jobs", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("jobs", &lwin, CIT_COMMAND));
 }
 
 TEARDOWN()
 {
 	vle_cmds_reset();
 	vle_keys_reset();
-	conf_teardown();
+	opt_handlers_teardown();
 
 	view_teardown(&lwin);
 
@@ -70,20 +71,28 @@ TEARDOWN()
 TEST(jobs_are_listed)
 {
 	assert_int_equal(1, menu_get_current()->len);
-	assert_string_equal("1/0       ", menu_get_current()->items[0]);
+	assert_string_equal("1/0       job", menu_get_current()->items[0]);
 }
 
 TEST(dd_press)
 {
 	(void)vle_keys_exec(WK_d WK_d);
-	assert_string_equal("1/0        (cancelling...)",
+	assert_string_equal("1/0       (cancelling...) job",
 			menu_get_current()->items[0]);
 }
 
+TEST(r_press)
+{
+	bg_job_t *job = bg_jobs;
+	replace_string(&job->cmd, "job_updated");
+	(void)vle_keys_exec(WK_r);
+	assert_string_equal("1/0       job_updated", menu_get_current()->items[0]);
+}
+
 TEST(e_press_without_errors)
 {
 	(void)vle_keys_exec(WK_e);
-	assert_string_equal("1/0       ", menu_get_current()->items[0]);
+	assert_string_equal("1/0       job", menu_get_current()->items[0]);
 }
 
 TEST(e_press_with_errors)
@@ -91,7 +100,7 @@ TEST(e_press_with_errors)
 	bg_job_t *job = bg_jobs;
 
 	(void)vle_keys_exec(WK_q);
-	assert_success(exec_commands("jobs", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("jobs", &lwin, CIT_COMMAND));
 
 	pthread_spin_lock(&job->errors_lock);
 	(void)strappend(&job->errors, &job->errors_len, "this\nis\nerror");
diff --git a/tests/misc/menus_map.c b/tests/menus/map.c
similarity index 75%
rename from tests/misc/menus_map.c
rename to tests/menus/map.c
index 96fb225..507a635 100644
--- a/tests/misc/menus_map.c
+++ b/tests/menus/map.c
@@ -16,8 +16,8 @@
 SETUP()
 {
 	conf_setup();
-	init_modes();
-	init_commands();
+	modes_init();
+	cmds_init();
 
 	curr_view = &lwin;
 	other_view = &rwin;
@@ -41,8 +41,8 @@ TEARDOWN()
 
 TEST(nop_rhs_is_displayed)
 {
-	assert_success(exec_commands("nmap lhs <nop>", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("nmap lhs", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nmap lhs <nop>", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nmap lhs", &lwin, CIT_COMMAND));
 
 	assert_int_equal(4, menu_get_current()->len);
 	assert_string_equal("User mappings:", menu_get_current()->items[0]);
@@ -50,13 +50,13 @@ TEST(nop_rhs_is_displayed)
 	assert_string_equal("", menu_get_current()->items[2]);
 	assert_string_equal("Builtin mappings:", menu_get_current()->items[3]);
 
-	abort_menu_like_mode();
+	modes_abort_menu_like();
 }
 
 TEST(space_in_rhs_is_displayed_without_notation)
 {
-	assert_success(exec_commands("nmap lhs s p a c e", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("nmap lhs", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nmap lhs s p a c e", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nmap lhs", &lwin, CIT_COMMAND));
 
 	assert_int_equal(4, menu_get_current()->len);
 	assert_string_equal("User mappings:", menu_get_current()->items[0]);
@@ -64,13 +64,13 @@ TEST(space_in_rhs_is_displayed_without_notation)
 	assert_string_equal("", menu_get_current()->items[2]);
 	assert_string_equal("Builtin mappings:", menu_get_current()->items[3]);
 
-	abort_menu_like_mode();
+	modes_abort_menu_like();
 }
 
 TEST(single_space_is_displayed_using_notation)
 {
-	assert_success(exec_commands("nmap <space> <space>", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("nmap <space>", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nmap <space> <space>", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nmap <space>", &lwin, CIT_COMMAND));
 
 	assert_int_equal(5, menu_get_current()->len);
 	assert_string_equal("User mappings:", menu_get_current()->items[0]);
@@ -79,15 +79,15 @@ TEST(single_space_is_displayed_using_notation)
 	assert_string_equal("Builtin mappings:", menu_get_current()->items[3]);
 	assert_string_equal("<space>     switch pane", menu_get_current()->items[4]);
 
-	abort_menu_like_mode();
+	modes_abort_menu_like();
 }
 
 TEST(first_or_last_space_is_displayed_using_notation)
 {
-	assert_success(exec_commands("nmap <space>1 <space>1", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("nmap <space>2 <space>2 x", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nmap <space>1 <space>1", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nmap <space>2 <space>2 x", &lwin, CIT_COMMAND));
 
-	assert_success(exec_commands("nmap <space>", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nmap <space>", &lwin, CIT_COMMAND));
 	assert_int_equal(6, menu_get_current()->len);
 	assert_string_equal("User mappings:", menu_get_current()->items[0]);
 	assert_string_equal("<space>1    <space>1", menu_get_current()->items[1]);
@@ -96,13 +96,13 @@ TEST(first_or_last_space_is_displayed_using_notation)
 	assert_string_equal("Builtin mappings:", menu_get_current()->items[4]);
 	assert_string_equal("<space>     switch pane", menu_get_current()->items[5]);
 
-	assert_success(exec_commands("nunmap <space>1", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("nunmap <space>2", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nunmap <space>1", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nunmap <space>2", &lwin, CIT_COMMAND));
 
-	assert_success(exec_commands("nmap x1<space> 1<space>", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("nmap x2<space> 2 x<space>", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nmap x1<space> 1<space>", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nmap x2<space> 2 x<space>", &lwin, CIT_COMMAND));
 
-	assert_success(exec_commands("nmap x", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nmap x", &lwin, CIT_COMMAND));
 	assert_int_equal(5, menu_get_current()->len);
 	assert_string_equal("User mappings:", menu_get_current()->items[0]);
 	assert_string_equal("x1<space>   1<space>", menu_get_current()->items[1]);
@@ -110,12 +110,12 @@ TEST(first_or_last_space_is_displayed_using_notation)
 	assert_string_equal("", menu_get_current()->items[3]);
 	assert_string_equal("Builtin mappings:", menu_get_current()->items[4]);
 
-	assert_success(exec_commands("nmap <space>1<space> <space>1<space>",
+	assert_success(cmds_dispatch("nmap <space>1<space> <space>1<space>",
 				&lwin, CIT_COMMAND));
-	assert_success(exec_commands("nmap <space>2<space>2<space> <space>2 x<space>",
+	assert_success(cmds_dispatch("nmap <space>2<space>2<space> <space>2 x<space>",
 				&lwin, CIT_COMMAND));
 
-	assert_success(exec_commands("nmap <space>", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nmap <space>", &lwin, CIT_COMMAND));
 	assert_int_equal(6, menu_get_current()->len);
 	assert_string_equal("User mappings:", menu_get_current()->items[0]);
 	assert_string_equal("<space>1<space> <space>1<space>",
@@ -126,12 +126,12 @@ TEST(first_or_last_space_is_displayed_using_notation)
 	assert_string_equal("Builtin mappings:", menu_get_current()->items[4]);
 	assert_string_equal("<space>     switch pane", menu_get_current()->items[5]);
 
-	abort_menu_like_mode();
+	modes_abort_menu_like();
 }
 
 TEST(builtin_key_description_is_displayed)
 {
-	assert_success(exec_commands("nmap j", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("nmap j", &lwin, CIT_COMMAND));
 
 	assert_int_equal(4, menu_get_current()->len);
 	assert_string_equal("User mappings:", menu_get_current()->items[0]);
@@ -140,7 +140,7 @@ TEST(builtin_key_description_is_displayed)
 	assert_string_equal("j           go to item below",
 			menu_get_current()->items[3]);
 
-	abort_menu_like_mode();
+	modes_abort_menu_like();
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/tests/misc/menus_media.c b/tests/menus/media.c
similarity index 91%
rename from tests/misc/menus_media.c
rename to tests/menus/media.c
index ac25baa..429d518 100644
--- a/tests/misc/menus_media.c
+++ b/tests/menus/media.c
@@ -39,14 +39,15 @@ SETUP_ONCE()
 
 SETUP()
 {
-	conf_setup();
-	init_modes();
-	init_commands();
-
 	curr_view = &lwin;
 	other_view = &rwin;
 	view_setup(&lwin);
 
+	/* The redraw code updates 'columns' and 'lines'. */
+	opt_handlers_setup();
+	modes_init();
+	cmds_init();
+
 	curr_stats.load_stage = -1;
 	curr_stats.save_msg = 0;
 
@@ -70,7 +71,7 @@ TEARDOWN()
 	restore_cwd(saved_cwd);
 
 	vle_keys_reset();
-	conf_teardown();
+	opt_handlers_teardown();
 
 	view_teardown(&lwin);
 	curr_view = NULL;
@@ -87,7 +88,7 @@ TEST(menu_not_created_if_no_devices)
 	      "nothing", fp);
 	fclose(fp);
 
-	assert_failure(exec_commands("media", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("media", &lwin, CIT_COMMAND));
 	assert_true(vle_mode_is(NORMAL_MODE));
 }
 
@@ -95,14 +96,14 @@ TEST(menu_aborts_if_mediaprg_is_not_set)
 {
 	update_string(&cfg.media_prg, "");
 
-	assert_success(exec_commands("media", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("media", &lwin, CIT_COMMAND));
 	assert_true(vle_mode_is(NORMAL_MODE));
 }
 
 TEST(script_failure_is_handled)
 {
 	update_string(&cfg.media_prg, "very/wrong/cmd/name");
-	assert_failure(exec_commands("media", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("media", &lwin, CIT_COMMAND));
 	assert_true(vle_mode_is(NORMAL_MODE));
 }
 
@@ -113,7 +114,7 @@ TEST(menu_is_loaded)
 	      "device=/dev/sdf", fp);
 	fclose(fp);
 
-	assert_success(exec_commands("media", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("media", &lwin, CIT_COMMAND));
 
 	assert_true(vle_mode_is(MENU_MODE));
 	(void)vle_keys_exec(WK_ESC);
@@ -138,7 +139,7 @@ TEST(entries_are_formatted_correctly)
 	      "label=ignored label\n", fp);
 	fclose(fp);
 
-	assert_success(exec_commands("media", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("media", &lwin, CIT_COMMAND));
 
 	assert_int_equal(10, menu_get_current()->len);
 
@@ -176,7 +177,7 @@ TEST(enter_navigates_to_mount_point)
 	            "mount-point=%s\n", sandbox);
 	fclose(fp);
 
-	assert_success(exec_commands("media", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("media", &lwin, CIT_COMMAND));
 
 	lwin.curr_dir[0] = '\0';
 	(void)vle_keys_exec(WK_j);
@@ -192,7 +193,7 @@ TEST(enter_navigates_to_mount_point_on_device_line)
 	            "mount-point=%s\n", sandbox);
 	fclose(fp);
 
-	assert_success(exec_commands("media", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("media", &lwin, CIT_COMMAND));
 
 	lwin.curr_dir[0] = '\0';
 	(void)vle_keys_exec(WK_CR);
@@ -209,7 +210,7 @@ TEST(enter_mounts_unmounted_device)
 	      "echo \"$@\" >> out\n", fp);
 	fclose(fp);
 
-	assert_success(exec_commands("media", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("media", &lwin, CIT_COMMAND));
 
 	(void)remove("out");
 
@@ -245,7 +246,7 @@ TEST(enter_does_nothing_on_device_lines_with_multiple_mounts)
 	      "mount-point=mount-point2\n", fp);
 	fclose(fp);
 
-	assert_success(exec_commands("media", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("media", &lwin, CIT_COMMAND));
 
 	lwin.curr_dir[0] = '\0';
 	(void)vle_keys_exec(WK_CR);
@@ -261,7 +262,7 @@ TEST(unhandled_key_is_ignored)
 	            "mount-point=%s\n", sandbox);
 	fclose(fp);
 
-	assert_success(exec_commands("media", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("media", &lwin, CIT_COMMAND));
 
 	(void)vle_keys_exec(WK_x);
 	(void)vle_keys_exec(WK_ESC);
@@ -276,7 +277,7 @@ TEST(r_reloads_list)
 	            "mount-point=%s\n", sandbox);
 	fclose(fp);
 
-	assert_success(exec_commands("media", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("media", &lwin, CIT_COMMAND));
 
 	fp = fopen("script", "w");
 	fputs("#!/bin/sh\n"
@@ -314,7 +315,7 @@ TEST(m_toggles_mounts)
 	      "echo \"$@\" >> out\n", fp);
 	fclose(fp);
 
-	assert_success(exec_commands("media", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("media", &lwin, CIT_COMMAND));
 
 	(void)remove("out");
 
@@ -384,7 +385,7 @@ TEST(mounting_failure_is_handled)
 	      "mount-point=mount-point1\n", fp);
 	fclose(fp);
 
-	assert_success(exec_commands("media", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("media", &lwin, CIT_COMMAND));
 
 	fp = fopen("script", "w");
 	fputs("#!/bin/sh\n"
@@ -422,7 +423,7 @@ TEST(mount_directory_is_left_before_unmounting)
 
 	strcpy(lwin.curr_dir, sandbox);
 	assert_success(chdir(sandbox));
-	assert_success(exec_commands("media", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("media", &lwin, CIT_COMMAND));
 
 	(void)vle_keys_exec(WK_j);
 	(void)vle_keys_exec(WK_m);
@@ -456,7 +457,7 @@ TEST(mount_matching_current_path_is_picked_by_default)
 	fclose(fp);
 
 	strcpy(lwin.curr_dir, sandbox);
-	assert_success(exec_commands("media", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("media", &lwin, CIT_COMMAND));
 
 	assert_int_equal(4, menu_get_current()->pos);
 
@@ -480,7 +481,7 @@ TEST(barckets_navigates_between_devices)
 	fclose(fp);
 
 	strcpy(lwin.curr_dir, sandbox);
-	assert_success(exec_commands("media", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("media", &lwin, CIT_COMMAND));
 
 	assert_int_equal(10, menu_get_current()->len);
 
diff --git a/tests/misc/menus_misc.c b/tests/menus/misc.c
similarity index 59%
rename from tests/misc/menus_misc.c
rename to tests/menus/misc.c
index 067a3d8..8c5b453 100644
--- a/tests/misc/menus_misc.c
+++ b/tests/menus/misc.c
@@ -5,6 +5,7 @@
 
 #include <test-utils.h>
 
+#include "../../src/compat/fs_limits.h"
 #include "../../src/cfg/config.h"
 #include "../../src/engine/keys.h"
 #include "../../src/modes/menu.h"
@@ -16,11 +17,13 @@
 #include "../../src/filelist.h"
 #include "../../src/status.h"
 
+/* This tests various menus and generic things that need mode activation. */
+
 SETUP()
 {
 	conf_setup();
-	init_modes();
-	init_commands();
+	modes_init();
+	cmds_init();
 
 	curr_view = &lwin;
 	other_view = &rwin;
@@ -47,7 +50,7 @@ TEST(enter_loads_selected_colorscheme)
 	make_abs_path(cfg.colors_dir, sizeof(cfg.colors_dir), TEST_DATA_PATH,
 			"color-schemes/", NULL);
 
-	assert_success(exec_commands("colorscheme", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("colorscheme", &lwin, CIT_COMMAND));
 
 	strcpy(cfg.cs.name, "test-scheme");
 	cfg.cs.color[WIN_COLOR].fg = 1;
@@ -65,11 +68,28 @@ TEST(enter_loads_selected_colorscheme)
 	(void)vle_keys_exec(WK_ESC);
 }
 
+TEST(menu_can_be_searched_interactively)
+{
+	cfg.inc_search = 1;
+
+	assert_success(cmds_dispatch("vifm", &lwin, CIT_COMMAND));
+	menu_data_t *menu = menu_get_current();
+
+	(void)vle_keys_exec_timed_out(L"/^Git info:");
+	assert_string_starts_with("Git info:", menu->items[menu->pos]);
+	(void)vle_keys_exec_timed_out(WK_C_u L"^Version:");
+	assert_string_starts_with("Version:", menu->items[menu->pos]);
+
+	(void)vle_keys_exec_timed_out(WK_ESC);
+	(void)vle_keys_exec_timed_out(WK_ESC);
+	cfg.inc_search = 0;
+}
+
 TEST(menu_is_built_from_a_command)
 {
 	undo_setup();
 
-	assert_success(exec_commands("!echo only-line %m", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("!echo only-line %m", &lwin, CIT_COMMAND));
 
 	assert_int_equal(1, menu_get_current()->len);
 	assert_string_equal("only-line", menu_get_current()->items[0]);
@@ -90,7 +110,7 @@ TEST(menu_is_built_from_a_command_with_input, IF(have_cat))
 	lwin.dir_entry[1].marked = 1;
 	lwin.pending_marking = 1;
 
-	assert_success(exec_commands("!cat %Pl%m", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("!cat %Pl%m", &lwin, CIT_COMMAND));
 
 	assert_int_equal(2, menu_get_current()->len);
 	assert_string_equal("/path/a", menu_get_current()->items[0]);
@@ -106,7 +126,7 @@ TEST(menu_is_turned_into_cv)
 	undo_setup();
 
 	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), TEST_DATA_PATH, "", NULL);
-	assert_success(exec_commands("!echo existing-files/a%M", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("!echo existing-files/a%M", &lwin, CIT_COMMAND));
 
 	(void)vle_keys_exec(WK_b);
 	assert_true(flist_custom_active(&lwin));
@@ -115,5 +135,31 @@ TEST(menu_is_turned_into_cv)
 	undo_teardown();
 }
 
+TEST(locate_menu_can_escape_args, IF(not_windows))
+{
+	char script_path[PATH_MAX + 1];
+	make_abs_path(script_path, sizeof(script_path), SANDBOX_PATH, "script", NULL);
+
+	char locate_prg[PATH_MAX + 1];
+	snprintf(locate_prg, sizeof(locate_prg), "%s", script_path);
+	update_string(&cfg.locate_prg, locate_prg);
+
+	create_executable(SANDBOX_PATH "/script");
+	make_file(SANDBOX_PATH "/script",
+			"#!/bin/sh\n"
+			"for arg; do echo \"$arg\"; done\n");
+
+	assert_success(cmds_dispatch("locate a  b", &lwin, CIT_COMMAND));
+	assert_int_equal(1, menu_get_current()->len);
+	assert_string_equal("a  b", menu_get_current()->items[0]);
+
+	assert_success(cmds_dispatch("locate -a  b", &lwin, CIT_COMMAND));
+	assert_int_equal(2, menu_get_current()->len);
+	assert_string_equal("-a", menu_get_current()->items[0]);
+	assert_string_equal("b", menu_get_current()->items[1]);
+
+	assert_success(remove(script_path));
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 : */
diff --git a/tests/misc/menus_plugins.c b/tests/menus/plugins.c
similarity index 76%
rename from tests/misc/menus_plugins.c
rename to tests/menus/plugins.c
index 74dc06d..5f41a06 100644
--- a/tests/misc/menus_plugins.c
+++ b/tests/menus/plugins.c
@@ -20,14 +20,15 @@
 
 SETUP()
 {
-	conf_setup();
-	init_modes();
-	init_commands();
-
 	curr_view = &lwin;
 	other_view = &rwin;
 	view_setup(&lwin);
 
+	/* The redraw code updates 'columns' and 'lines'. */
+	opt_handlers_setup();
+	modes_init();
+	cmds_init();
+
 	curr_stats.load_stage = -1;
 
 	curr_stats.vlua = vlua_init();
@@ -51,7 +52,7 @@ TEARDOWN()
 
 	vle_cmds_reset();
 	vle_keys_reset();
-	conf_teardown();
+	opt_handlers_teardown();
 
 	view_teardown(&lwin);
 
@@ -70,28 +71,28 @@ TEST(plugins_are_listed)
 		assert_success(make_symlink("plug1", SANDBOX_PATH "/plugins/plug3"));
 	}
 
-	plugs_load(curr_stats.plugs, cfg.config_dir);
+	load_plugins(curr_stats.plugs, cfg.config_dir);
 	plugs_sort(curr_stats.plugs);
-	assert_success(exec_commands("plugins", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("plugins", &lwin, CIT_COMMAND));
 
 	int first_loaded = starts_with(menu_get_current()->items[0], "[ loaded] ");
 
 	assert_int_equal(not_windows() ? 3 : 2, menu_get_current()->len);
-	assert_true(starts_with(menu_get_current()->items[0],
-				first_loaded ? "[ loaded] " : "[skipped] "));
-	assert_true(starts_with(menu_get_current()->items[1], "[ failed] "));
+	assert_string_starts_with(first_loaded ? "[ loaded] " : "[skipped] ",
+			menu_get_current()->items[0]);
+	assert_string_starts_with("[ failed] ", menu_get_current()->items[1]);
 	if(not_windows())
 	{
-		assert_true(starts_with(menu_get_current()->items[2],
-				first_loaded ? "[skipped] " : "[ loaded] "));
+		assert_string_starts_with(first_loaded ? "[skipped] " : "[ loaded] ",
+				menu_get_current()->items[2]);
 		remove_file(SANDBOX_PATH "/plugins/plug3");
 	}
 }
 
 TEST(gf_press)
 {
-	plugs_load(curr_stats.plugs, cfg.config_dir);
-	assert_success(exec_commands("plugins", &lwin, CIT_COMMAND));
+	load_plugins(curr_stats.plugs, cfg.config_dir);
+	assert_success(cmds_dispatch("plugins", &lwin, CIT_COMMAND));
 
 	char *saved_cwd = save_cwd();
 
@@ -114,11 +115,11 @@ TEST(gf_press)
 
 TEST(e_press_without_errors)
 {
-	plugs_load(curr_stats.plugs, cfg.config_dir);
-	assert_success(exec_commands("plugins", &lwin, CIT_COMMAND));
+	load_plugins(curr_stats.plugs, cfg.config_dir);
+	assert_success(cmds_dispatch("plugins", &lwin, CIT_COMMAND));
 
 	(void)vle_keys_exec(WK_e);
-	assert_true(starts_with_lit(menu_get_current()->items[0], "["));
+	assert_string_starts_with("[", menu_get_current()->items[0]);
 }
 
 TEST(e_press_with_errors)
@@ -129,8 +130,8 @@ TEST(e_press_with_errors)
 	                                                  "print 'err2'\n"
 	                                                  "return");
 
-	plugs_load(curr_stats.plugs, cfg.config_dir);
-	assert_success(exec_commands("plugins", &lwin, CIT_COMMAND));
+	load_plugins(curr_stats.plugs, cfg.config_dir);
+	assert_success(cmds_dispatch("plugins", &lwin, CIT_COMMAND));
 
 	(void)vle_keys_exec(WK_e);
 	assert_string_equal("err1", menu_get_current()->items[0]);
@@ -140,7 +141,7 @@ TEST(e_press_with_errors)
 	(void)vle_keys_exec(WK_x);
 
 	(void)vle_keys_exec(WK_h);
-	assert_true(starts_with_lit(menu_get_current()->items[0], "["));
+	assert_string_starts_with("[", menu_get_current()->items[0]);
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/tests/misc/menus.c b/tests/menus/search.c
similarity index 79%
rename from tests/misc/menus.c
rename to tests/menus/search.c
index ad8c043..75201db 100644
--- a/tests/misc/menus.c
+++ b/tests/menus/search.c
@@ -1,20 +1,16 @@
 #include <stic.h>
 
-#include <unistd.h> /* chdir() symlink() */
-
-#include <stdlib.h> /* remove() */
-#include <string.h> /* strcpy() strdup() */
+#include <string.h> /* strdup() */
 
 #include <test-utils.h>
 
 #include "../../src/cfg/config.h"
-#include "../../src/compat/fs_limits.h"
 #include "../../src/menus/menus.h"
-#include "../../src/modes/modes.h"
 #include "../../src/ui/ui.h"
-#include "../../src/utils/fs.h"
 #include "../../src/utils/string_array.h"
 
+/* This tests search without activating the mode. */
+
 static menu_data_t m;
 
 SETUP()
@@ -31,41 +27,17 @@ TEARDOWN()
 	menus_reset_data(&m);
 }
 
-TEST(can_navigate_to_broken_symlink, IF(not_windows))
-{
-	char *saved_cwd;
-	char buf[PATH_MAX + 1];
-
-	strcpy(lwin.curr_dir, ".");
-
-	saved_cwd = save_cwd();
-	assert_success(chdir(SANDBOX_PATH));
-
-	/* symlink() is not available on Windows, but other code is fine. */
-#ifndef _WIN32
-	assert_success(symlink("/wrong/path", "broken-link"));
-#endif
-
-	make_abs_path(buf, sizeof(buf), SANDBOX_PATH , "broken-link:", saved_cwd);
-	/* Were trying to open broken link, which will fail, but the parsing part
-	 * should succeed. */
-	restore_cwd(saved_cwd);
-	assert_success(menus_goto_file(&m, &lwin, buf, 1));
-
-	assert_success(remove(SANDBOX_PATH "/broken-link"));
-}
-
 TEST(nothing_is_searched_if_no_pattern)
 {
 	menus_search_repeat(m.state, 0);
-	assert_int_equal(0, menus_search_matched(m.state));
+	assert_int_equal(0, menus_search_matched(&m));
 }
 
 TEST(nothing_is_searched_for_wrong_pattern)
 {
 	menus_search_reset(m.state, 0, 1);
 	assert_true(menus_search("*a", &m, 1));
-	assert_int_equal(0, menus_search_matched(m.state));
+	assert_int_equal(0, menus_search_matched(&m));
 }
 
 TEST(search_via_menu_search)
diff --git a/tests/menus/suite.c b/tests/menus/suite.c
new file mode 100644
index 0000000..0aba25c
--- /dev/null
+++ b/tests/menus/suite.c
@@ -0,0 +1,45 @@
+#include <stic.h>
+
+#include <string.h> /* strcpy() */
+
+#include <test-utils.h>
+
+#include "../../src/ui/color_manager.h"
+#include "../../src/ui/tabs.h"
+#include "../../src/ui/ui.h"
+#include "../../src/utils/fs.h"
+#include "../../src/background.h"
+
+static char *saved_cwd;
+
+DEFINE_SUITE();
+
+SETUP_ONCE()
+{
+	fix_environ();
+
+	stub_colmgr();
+
+	/* Remember original path in global SETUP_ONCE instead of SETUP to make sure
+	 * nothing will change the path before we try to save it. */
+	saved_cwd = save_cwd();
+
+	assert_success(bg_init());
+
+	tabs_init();
+}
+
+SETUP()
+{
+	strcpy(lwin.curr_dir, "/non-existing-dir");
+	strcpy(rwin.curr_dir, "/non-existing-dir");
+}
+
+TEARDOWN()
+{
+	restore_cwd(saved_cwd);
+	saved_cwd = save_cwd();
+}
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/misc/menus_undolist.c b/tests/menus/undolist.c
similarity index 99%
rename from tests/misc/menus_undolist.c
rename to tests/menus/undolist.c
index 7cf405d..a94f54a 100644
--- a/tests/misc/menus_undolist.c
+++ b/tests/menus/undolist.c
@@ -18,7 +18,7 @@ SETUP()
 	static int undo_levels = 10;
 	un_init(&exec_func, &op_avail, NULL, &undo_levels);
 
-	init_modes();
+	modes_init();
 
 	curr_stats.load_stage = -1;
 }
diff --git a/tests/misc/menus_users.c b/tests/menus/users.c
similarity index 94%
rename from tests/misc/menus_users.c
rename to tests/menus/users.c
index 84c7ab6..597de32 100644
--- a/tests/misc/menus_users.c
+++ b/tests/menus/users.c
@@ -15,8 +15,8 @@
 
 SETUP()
 {
-	init_modes();
-	init_commands();
+	modes_init();
+	cmds_init();
 	conf_setup();
 	undo_setup();
 
@@ -38,7 +38,7 @@ TEARDOWN()
 
 TEST(v_key)
 {
-	assert_success(exec_commands("!echo path %M", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("!echo path %M", &lwin, CIT_COMMAND));
 
 	curr_stats.vlua = vlua_init();
 
diff --git a/tests/misc/args.c b/tests/misc/args.c
index fdedd32..6ba5d01 100644
--- a/tests/misc/args.c
+++ b/tests/misc/args.c
@@ -177,6 +177,27 @@ TEST(various_flags)
 	args_free(&args);
 }
 
+TEST(plugins_dir)
+{
+	args_t args = { };
+	char *argv[] = { "vifm", "--plugins-dir=a", "--plugins-dir", "/b/c", NULL };
+
+	args_parse(&args, ARRAY_LEN(argv) - 1U, argv, "/");
+
+	assert_int_equal(2, args.nplugins_dirs);
+	assert_string_equal("/a", args.plugins_dirs[0]);
+	assert_string_equal("/b/c", args.plugins_dirs[1]);
+
+	args_process(&args, AS_OTHER, /*ipc=*/NULL);
+
+	/* Paths are stored in the order in which they will be processed. */
+	assert_int_equal(2, curr_stats.plugins_dirs.nitems);
+	assert_string_equal("/b/c", curr_stats.plugins_dirs.items[0]);
+	assert_string_equal("/a", curr_stats.plugins_dirs.items[1]);
+
+	args_free(&args);
+}
+
 TEST(remote_allows_no_arguments, IF(with_remote_cmds))
 {
 	args_t args = { };
diff --git a/tests/misc/background.c b/tests/misc/background.c
index 8b12e73..70c2e20 100644
--- a/tests/misc/background.c
+++ b/tests/misc/background.c
@@ -10,13 +10,13 @@
 #include "../../src/engine/var.h"
 #include "../../src/engine/variables.h"
 #include "../../src/utils/cancellation.h"
-#include "../../src/utils/str.h"
 #include "../../src/utils/string_array.h"
 #include "../../src/ui/ui.h"
 #include "../../src/background.h"
 #include "../../src/signals.h"
 #include "../../src/status.h"
 
+static void on_job_exit(struct bg_job_t *job, void *data);
 static void task(bg_op_t *bg_op, void *arg);
 static void wait_until_locked(pthread_spinlock_t *lock);
 
@@ -63,7 +63,7 @@ TEST(capture_error_of_external_command)
 			usleep(5000);
 			continue;
 		}
-		assert_true(starts_with_lit(job->errors, "there"));
+		assert_string_starts_with("there", job->errors);
 		pthread_spin_unlock(&job->errors_lock);
 		break;
 	}
@@ -199,6 +199,38 @@ TEST(capture_output_of_external_command)
 	bg_job_decref(job);
 }
 
+TEST(jobs_exit_cb_is_called)
+{
+	bg_job_t *job = bg_run_external_job("echo there", BJF_NONE);
+	assert_non_null(job);
+
+	int called = 0;
+	bg_job_set_exit_cb(job, &on_job_exit, &called);
+
+	int counter = 0;
+	while(bg_job_is_running(job))
+	{
+		usleep(5000);
+		bg_check();
+		if(++counter > 100)
+		{
+			assert_fail("Waiting for too long.");
+			return;
+		}
+	}
+
+	assert_int_equal(1, called);
+
+	bg_job_decref(job);
+}
+
+static void
+on_job_exit(struct bg_job_t *job, void *data)
+{
+	int *called = data;
+	*called = 1;
+}
+
 TEST(supply_input_to_external_command, IF(have_cat))
 {
 	bg_job_t *job = bg_run_external_job("cat",
diff --git a/tests/misc/builtin_functions.c b/tests/misc/builtin_functions.c
index a9d07e1..e4fcf23 100644
--- a/tests/misc/builtin_functions.c
+++ b/tests/misc/builtin_functions.c
@@ -33,7 +33,7 @@ SETUP()
 	update_string(&cfg.shell_cmd_flag, "-c");
 
 	init_builtin_functions();
-	init_parser(NULL);
+	vle_parser_init(NULL);
 	init_variables();
 
 	view_setup(&lwin);
@@ -73,6 +73,13 @@ TEST(executable_false_for_dir)
 	ASSERT_INT_OK("executable('.')", 0);
 }
 
+TEST(filereadable)
+{
+	ASSERT_INT_OK("filereadable('" TEST_DATA_PATH "/read/two-lines')", 1);
+	ASSERT_INT_OK("filereadable('" TEST_DATA_PATH "/read')", 0);
+	ASSERT_INT_OK("filereadable('no-such-path')", 0);
+}
+
 TEST(expand_expands_environment_variables)
 {
 	let_variables("$OPEN_ME = 'Found something interesting?'");
@@ -265,7 +272,7 @@ TEST(getpanetype_for_compare_view)
 	other_view = &rwin;
 
 	opt_handlers_setup();
-	compare_one_pane(&lwin, CT_CONTENTS, LT_DUPS, 0);
+	compare_one_pane(&lwin, CT_CONTENTS, LT_DUPS, CF_NONE);
 	opt_handlers_teardown();
 
 	ASSERT_OK("getpanetype()", "compare");
@@ -481,5 +488,10 @@ TEST(extcached)
 	set_extcached_monitor_type(FMT_CHANGED);
 }
 
+TEST(input_wrong_arg)
+{
+	ASSERT_FAIL("input('prompt', 'input', 'bla')", PE_INVALID_EXPRESSION);
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/misc/chase_links.c b/tests/misc/chase_links.c
index e0cd731..6346c6c 100644
--- a/tests/misc/chase_links.c
+++ b/tests/misc/chase_links.c
@@ -30,7 +30,7 @@ SETUP()
 	curr_view = &lwin;
 	other_view = &rwin;
 
-	init_commands();
+	cmds_init();
 
 	cfg.slow_fs_list = strdup("");
 	cfg.chase_links = 1;
diff --git a/tests/misc/cline.c b/tests/misc/cline.c
index 44c7343..0bb9767 100644
--- a/tests/misc/cline.c
+++ b/tests/misc/cline.c
@@ -22,5 +22,30 @@ TEST(left_ellipsis_for_cline)
 	cline_dispose(&cline);
 }
 
+TEST(append_works)
+{
+	cline_t left = {
+		.line = strdup("left"),
+		.line_len = strlen(left.line),
+		.attrs = strdup(""),
+		.attrs_len = strlen(left.attrs),
+	};
+
+	cline_t right = {
+		.line = strdup("right"),
+		.line_len = strlen(right.line),
+		.attrs = strdup(""),
+		.attrs_len = strlen(right.attrs),
+	};
+
+	cline_append(&left, &right);
+	assert_string_equal("leftright", left.line);
+	assert_int_equal(strlen(left.line), left.line_len);
+	assert_string_equal("    ", left.attrs);
+	assert_int_equal(strlen(left.attrs), left.attrs_len);
+
+	cline_dispose(&left);
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 : */
diff --git a/tests/misc/cmdline.c b/tests/misc/cmdline.c
new file mode 100644
index 0000000..6256c74
--- /dev/null
+++ b/tests/misc/cmdline.c
@@ -0,0 +1,606 @@
+#include <stic.h>
+
+#include <test-utils.h>
+
+#include "../../src/cfg/config.h"
+#include "../../src/compat/curses.h"
+#include "../../src/compat/os.h"
+#include "../../src/engine/keys.h"
+#include "../../src/engine/mode.h"
+#include "../../src/modes/cmdline.h"
+#include "../../src/modes/modes.h"
+#include "../../src/modes/wk.h"
+#include "../../src/ui/column_view.h"
+#include "../../src/ui/statusbar.h"
+#include "../../src/ui/ui.h"
+#include "../../src/utils/path.h"
+#include "../../src/utils/str.h"
+#include "../../src/builtin_functions.h"
+#include "../../src/cmd_core.h"
+#include "../../src/event_loop.h"
+#include "../../src/filelist.h"
+#include "../../src/flist_hist.h"
+#include "../../src/status.h"
+
+static void prompt_callback(const char response[], void *arg);
+static void column_line_print(const char buf[], size_t offset, AlignType align,
+		const char full_column[], const format_info_t *info);
+
+static line_stats_t *stats;
+
+static char *prompt_response;
+static int prompt_invocation_count;
+
+SETUP_ONCE()
+{
+	stats = get_line_stats();
+	try_enable_utf8_locale();
+	init_builtin_functions();
+}
+
+SETUP()
+{
+	modes_init();
+
+	curr_view = &lwin;
+	other_view = &rwin;
+
+	view_setup(&lwin);
+	view_setup(&rwin);
+}
+
+TEARDOWN()
+{
+	view_teardown(&lwin);
+	view_teardown(&rwin);
+
+	(void)vle_keys_exec_timed_out(WK_C_c);
+
+	vle_keys_reset();
+	cfg.inc_search = 0;
+}
+
+TEST(expr_reg_completion)
+{
+	(void)vle_keys_exec_timed_out(L":" WK_C_r WK_EQUALS);
+	(void)vle_keys_exec_timed_out(L"ex" WK_C_i);
+	assert_wstring_equal(L"executable(", stats->line);
+	(void)vle_keys_exec_timed_out(WK_C_c);
+}
+
+TEST(expr_reg_completion_ignores_pipe)
+{
+	(void)vle_keys_exec_timed_out(L":" WK_C_r WK_EQUALS);
+	(void)vle_keys_exec_timed_out(L"ab|ex" WK_C_i);
+	assert_wstring_equal(L"ab|ex", stats->line);
+	(void)vle_keys_exec_timed_out(WK_C_c);
+}
+
+TEST(prompt_cb_is_called_on_success)
+{
+	update_string(&prompt_response, NULL);
+	prompt_invocation_count = 0;
+
+	modcline_prompt("(prompt)", "initial", &prompt_callback, /*cb_arg=*/NULL,
+			/*complete=*/NULL, /*allow_ee=*/0);
+	assert_true(vle_mode_is(CMDLINE_MODE));
+	assert_int_equal(CLS_PROMPT, stats->sub_mode);
+	(void)vle_keys_exec_timed_out(WK_CR);
+
+	assert_string_equal("initial", prompt_response);
+	assert_int_equal(1, prompt_invocation_count);
+}
+
+TEST(prompt_cb_is_called_on_cancellation)
+{
+	update_string(&prompt_response, NULL);
+	prompt_invocation_count = 0;
+
+	modcline_prompt("(prompt)", "initial", &prompt_callback, /*cb_arg=*/NULL,
+			/*complete=*/NULL, /*allow_ee=*/0);
+	assert_true(vle_mode_is(CMDLINE_MODE));
+	assert_int_equal(CLS_PROMPT, stats->sub_mode);
+	(void)vle_keys_exec_timed_out(WK_C_c);
+
+	assert_string_equal(NULL, prompt_response);
+	assert_int_equal(1, prompt_invocation_count);
+}
+
+TEST(user_prompt_accepts_input)
+{
+	cfg.timeout_len = 1;
+	ui_sb_msg("");
+
+	/* Preparing input beforehand, because input() runs nested event loop. */
+	feed_keys(L"suffix" WK_CR);
+	(void)vle_keys_exec_timed_out(L":echo input('prompt', 'input')" WK_CR);
+
+	assert_string_equal("inputsuffix", ui_sb_last());
+}
+
+TEST(user_prompt_handles_cancellation)
+{
+	cfg.timeout_len = 1;
+	ui_sb_msg("old");
+
+	/* Preparing input beforehand, because input() runs nested event loop. */
+	feed_keys(L"suffix" WK_C_c);
+	(void)vle_keys_exec_timed_out(L":echo input('prompt', 'input')" WK_CR);
+
+	assert_string_equal("", ui_sb_last());
+}
+
+TEST(user_prompt_nests)
+{
+	cfg.timeout_len = 1;
+	ui_sb_msg("");
+
+	/* Preparing input beforehand, because input() runs nested event loop. */
+	feed_keys(L"-" WK_CR L"*" WK_CR);
+	(void)vle_keys_exec_timed_out(
+			L":echo input('p2', input('p1', '1').'2')" WK_CR);
+
+	assert_string_equal("1-2*", ui_sb_last());
+}
+
+TEST(user_prompt_and_expr_reg)
+{
+	cfg.timeout_len = 1;
+	ui_sb_msg("");
+
+	/* Preparing input beforehand, because input() runs nested event loop. */
+	feed_keys(WK_C_r WK_EQUALS
+			"input('n')" WK_CR
+			L"nested" WK_CR
+			L"extra" WK_CR);
+	(void)vle_keys_exec_timed_out(L":echo input('p').'out'" WK_CR);
+
+	assert_string_equal("nestedextraout", ui_sb_last());
+}
+
+TEST(user_prompt_completion)
+{
+	cfg.timeout_len = 1;
+	ui_sb_msg("");
+	make_abs_path(curr_view->curr_dir, sizeof(curr_view->curr_dir),
+			TEST_DATA_PATH, "", NULL);
+
+	/* Preparing input beforehand, because input() runs nested event loop. */
+	feed_keys(WK_C_i WK_CR);
+	(void)vle_keys_exec_timed_out(L":echo input('p', 'read/dos', 'dir')" WK_CR);
+	assert_string_equal("read/dos", ui_sb_last());
+
+	/* Preparing input beforehand, because input() runs nested event loop. */
+	feed_keys(WK_C_i WK_CR);
+	(void)vle_keys_exec_timed_out(L":echo input('p', 'read/dos', 'file')" WK_CR);
+	assert_string_equal("read/dos-eof", ui_sb_last());
+}
+
+TEST(each_filtering_prompt_gets_clean_state)
+{
+	conf_setup();
+	cfg.inc_search = 1;
+
+	/* In order to handle input change, old input must be stored somewhere and
+	 * compared against the new state.  Make sure the storage is cleared on every
+	 * prompt. */
+
+	(void)vle_keys_exec_timed_out(L"=a");
+	assert_string_equal("a", curr_view->local_filter.filter.raw);
+	(void)vle_keys_exec_timed_out(WK_ESC);
+	assert_string_equal("", curr_view->local_filter.filter.raw);
+
+	(void)vle_keys_exec_timed_out(L"=a");
+	assert_string_equal("a", curr_view->local_filter.filter.raw);
+	(void)vle_keys_exec_timed_out(WK_ESC);
+
+	conf_teardown();
+}
+
+TEST(cmdline_navigation)
+{
+	/* This doesn't work outside of search and local filter submodes. */
+	make_abs_path(curr_view->curr_dir, sizeof(curr_view->curr_dir),
+			TEST_DATA_PATH, "tree", NULL);
+	(void)vle_keys_exec_timed_out(L":");
+
+	(void)vle_keys_exec_timed_out(WK_C_y);
+	assert_false(stats->navigating);
+
+	(void)vle_keys_exec_timed_out(WK_C_o);
+	assert_string_equal("tree", get_last_path_component(curr_view->curr_dir));
+}
+
+TEST(navigation_requires_interactivity)
+{
+	(void)vle_keys_exec_timed_out(L"/");
+
+	cfg.inc_search = 0;
+	(void)vle_keys_exec_timed_out(WK_C_y);
+	assert_false(stats->navigating);
+
+	cfg.inc_search = 1;
+	(void)vle_keys_exec_timed_out(WK_C_y);
+	assert_true(stats->navigating);
+	(void)vle_keys_exec_timed_out(WK_C_y);
+	assert_false(stats->navigating);
+}
+
+TEST(navigation_movement)
+{
+	conf_setup();
+	cfg.inc_search = 1;
+
+	make_abs_path(curr_view->curr_dir, sizeof(curr_view->curr_dir),
+			TEST_DATA_PATH, "read", NULL);
+	populate_dir_list(curr_view, /*reload=*/0);
+
+	(void)vle_keys_exec_timed_out(L"/");
+
+	(void)vle_keys_exec_timed_out(WK_C_y);
+	assert_string_equal("binary-data",
+			curr_view->dir_entry[curr_view->list_pos].name);
+	(void)vle_keys_exec_timed_out(WK_C_n);
+	assert_string_equal("dos-eof",
+			curr_view->dir_entry[curr_view->list_pos].name);
+	(void)vle_keys_exec_timed_out(WK_C_n);
+	assert_string_equal("dos-line-endings",
+			curr_view->dir_entry[curr_view->list_pos].name);
+	(void)vle_keys_exec_timed_out(WK_C_p);
+	assert_string_equal("dos-eof",
+			curr_view->dir_entry[curr_view->list_pos].name);
+
+#ifdef ENABLE_EXTENDED_KEYS
+	wchar_t keys[2] = { };
+
+	keys[0] = K(KEY_UP);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_string_equal("binary-data",
+			curr_view->dir_entry[curr_view->list_pos].name);
+	keys[0] = K(KEY_DOWN);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_string_equal("dos-eof",
+			curr_view->dir_entry[curr_view->list_pos].name);
+	keys[0] = K(KEY_HOME);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_string_equal("binary-data",
+			curr_view->dir_entry[curr_view->list_pos].name);
+	keys[0] = K(KEY_END);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_string_equal("very-long-line",
+			curr_view->dir_entry[curr_view->list_pos].name);
+	keys[0] = K(KEY_LEFT);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_string_equal("test-data",
+			get_last_path_component(curr_view->curr_dir));
+	keys[0] = K(KEY_RIGHT);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_string_equal("read", get_last_path_component(curr_view->curr_dir));
+
+	/* Setup for scrolling. */
+	curr_view->window_rows = 5;
+	setup_grid(curr_view, /*column_count=*/1, curr_view->list_rows, /*init=*/0);
+	curr_view->top_line = 1;
+	curr_view->list_pos = curr_view->list_rows - 1;
+
+	keys[0] = K(KEY_PPAGE);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_string_equal("dos-line-endings",
+			curr_view->dir_entry[curr_view->list_pos].name);
+	keys[0] = K(KEY_NPAGE);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_string_equal("two-lines",
+			curr_view->dir_entry[curr_view->list_pos].name);
+#endif
+
+	conf_teardown();
+}
+
+TEST(search_navigation)
+{
+	conf_setup();
+	histories_init(5);
+	cfg.inc_search = 1;
+	cfg.wrap_scan = 1;
+
+	make_abs_path(curr_view->curr_dir, sizeof(curr_view->curr_dir),
+			TEST_DATA_PATH, "tree", NULL);
+	populate_dir_list(curr_view, /*reload=*/0);
+
+	(void)vle_keys_exec_timed_out(L"/" WK_C_y);
+
+	/* Can enter and leave directories. */
+	(void)vle_keys_exec_timed_out(L"5" WK_C_m);
+	assert_string_equal("dir5", get_last_path_component(curr_view->curr_dir));
+	(void)vle_keys_exec_timed_out(WK_C_o);
+	assert_string_equal("tree", get_last_path_component(curr_view->curr_dir));
+	(void)vle_keys_exec_timed_out(L"1" WK_C_m);
+	assert_string_equal("dir1", get_last_path_component(curr_view->curr_dir));
+	(void)vle_keys_exec_timed_out(L"2" WK_C_m);
+	assert_string_equal("dir2", get_last_path_component(curr_view->curr_dir));
+
+	assert_true(hist_is_empty(&curr_stats.search_hist));
+
+	cfg.wrap_scan = 0;
+	histories_init(0);
+	conf_teardown();
+}
+
+/* Same as search_navigation test above.  Duplicating because filtering is more
+ * complicated and it's a good idea to verify it also works fine. */
+TEST(filter_navigation)
+{
+	conf_setup();
+	histories_init(5);
+	cfg.inc_search = 1;
+	cfg.wrap_scan = 1;
+
+	make_abs_path(curr_view->curr_dir, sizeof(curr_view->curr_dir),
+			TEST_DATA_PATH, "tree", NULL);
+	populate_dir_list(curr_view, /*reload=*/0);
+
+	(void)vle_keys_exec_timed_out(L"=" WK_C_y);
+
+	/* Can enter and leave directories. */
+	(void)vle_keys_exec_timed_out(L"5" WK_C_m);
+	assert_string_equal("dir5", get_last_path_component(curr_view->curr_dir));
+	(void)vle_keys_exec_timed_out(WK_C_o);
+	assert_string_equal("tree", get_last_path_component(curr_view->curr_dir));
+	(void)vle_keys_exec_timed_out(L"1" WK_C_m);
+	assert_string_equal("dir1", get_last_path_component(curr_view->curr_dir));
+	(void)vle_keys_exec_timed_out(L"2" WK_C_m);
+	assert_string_equal("dir2", get_last_path_component(curr_view->curr_dir));
+
+	assert_true(hist_is_empty(&curr_stats.filter_hist));
+
+	cfg.wrap_scan = 0;
+	histories_init(0);
+	conf_teardown();
+}
+
+TEST(can_leave_navigation_without_running)
+{
+	conf_setup();
+	cfg.inc_search = 1;
+	cfg.wrap_scan = 1;
+
+	make_abs_path(curr_view->curr_dir, sizeof(curr_view->curr_dir),
+			TEST_DATA_PATH, "tree", NULL);
+	populate_dir_list(curr_view, /*reload=*/0);
+
+	(void)vle_keys_exec_timed_out(L"=" WK_C_y L"h" WK_C_j);
+	assert_int_equal(1, curr_view->list_rows);
+
+	cfg.wrap_scan = 0;
+	conf_teardown();
+}
+
+TEST(normal_in_autocmd_does_not_break_filter_navigation)
+{
+	conf_setup();
+	assert_success(stats_init(&cfg));
+	cfg.inc_search = 1;
+
+	assert_success(cmds_dispatch1("autocmd DirEnter * normal ga", curr_view,
+				CIT_COMMAND));
+
+	make_abs_path(curr_view->curr_dir, sizeof(curr_view->curr_dir),
+			TEST_DATA_PATH, "tree", NULL);
+	populate_dir_list(curr_view, /*reload=*/0);
+
+	(void)vle_keys_exec_timed_out(L"=" WK_C_y WK_C_m);
+	assert_string_equal("", curr_view->local_filter.filter.raw);
+
+	assert_success(cmds_dispatch1("autocmd!", curr_view, CIT_COMMAND));
+	wait_for_bg();
+
+	conf_teardown();
+}
+
+TEST(filter_in_autocmd_does_not_break_filter_navigation)
+{
+	conf_setup();
+	histories_init(5);
+	cfg.inc_search = 1;
+	cfg.auto_ch_pos = 1;
+	cfg.ch_pos_on = CHPOS_ENTER;
+
+	columns_setup_column(SK_BY_NAME);
+	columns_setup_column(SK_BY_SIZE);
+	columns_set_line_print_func(&column_line_print);
+	curr_view->columns = columns_create();
+	opt_handlers_setup();
+
+	assert_success(cmds_dispatch1("autocmd DirEnter tree/ filter! dir", curr_view,
+				CIT_COMMAND));
+
+	make_abs_path(curr_view->curr_dir, sizeof(curr_view->curr_dir),
+			TEST_DATA_PATH, "tree", NULL);
+	populate_dir_list(curr_view, /*reload=*/0);
+	assert_int_equal(3, curr_view->list_rows);
+	curr_view->list_pos = 2;
+	flist_hist_save(curr_view);
+
+	curr_stats.load_stage = 2;
+	(void)vle_keys_exec_timed_out(L"h/" WK_C_y WK_C_m);
+	/* This shouldn't lead to assertion/segfault caused by cursor position being
+	 * outside of file list. */
+	(void)vle_keys_exec_timed_out(L"d");
+	curr_stats.load_stage = 0;
+	assert_int_equal(1, curr_view->list_rows);
+	/* Position of the filtered list is the one being stored. */
+	assert_int_equal(0, stats->old_top);
+	assert_int_equal(0, stats->old_pos);
+
+	(void)vle_keys_exec_timed_out(WK_C_c);
+	assert_success(cmds_dispatch1("autocmd!", curr_view, CIT_COMMAND));
+
+	opt_handlers_teardown();
+	columns_teardown();
+
+	histories_init(0);
+	conf_teardown();
+	cfg.auto_ch_pos = 0;
+	cfg.ch_pos_on = 0;
+}
+
+TEST(filtering_does_not_result_in_invalid_position)
+{
+	conf_setup();
+	histories_init(5);
+	cfg.inc_search = 1;
+
+	make_abs_path(curr_view->curr_dir, sizeof(curr_view->curr_dir),
+			TEST_DATA_PATH, "tree", NULL);
+	populate_dir_list(curr_view, /*reload=*/0);
+	assert_int_equal(3, curr_view->list_rows);
+	flist_hist_save(curr_view);
+
+	(void)vle_keys_exec_timed_out(L"=5" WK_C_m);
+	assert_int_equal(1, curr_view->list_rows);
+	(void)vle_keys_exec_timed_out(L"=" WK_C_u WK_C_y);
+	assert_int_equal(3, curr_view->list_rows);
+	/* This shouldn't cause a crash because of incorrect cursor position (outside
+	 * of file list). */
+	(void)vle_keys_exec_timed_out(WK_C_o);
+
+	(void)vle_keys_exec_timed_out(WK_C_c);
+
+	opt_handlers_teardown();
+	columns_teardown();
+
+	histories_init(0);
+	conf_teardown();
+}
+
+TEST(leaving_navigation_does_not_move_cursor)
+{
+	conf_setup();
+	cfg.inc_search = 1;
+
+	make_abs_path(curr_view->curr_dir, sizeof(curr_view->curr_dir),
+			TEST_DATA_PATH, "existing-files", NULL);
+	populate_dir_list(curr_view, /*reload=*/0);
+
+	/* Search. */
+	curr_view->list_pos = 0;
+	(void)vle_keys_exec_timed_out(L"/");
+	/* Empty input. */
+	(void)vle_keys_exec_timed_out(WK_C_y);
+	assert_int_equal(0, curr_view->list_pos);
+	(void)vle_keys_exec_timed_out(WK_C_n);
+	assert_int_equal(1, curr_view->list_pos);
+	(void)vle_keys_exec_timed_out(WK_C_y);
+	assert_int_equal(1, curr_view->list_pos);
+	/* This triggers view cursor update. */
+	(void)vle_keys_exec_timed_out(L"a" WK_C_h);
+	assert_int_equal(1, curr_view->list_pos);
+	/* Non-empty input. */
+	(void)vle_keys_exec_timed_out(WK_C_y L"a");
+	assert_int_equal(1, curr_view->list_pos);
+	(void)vle_keys_exec_timed_out(WK_C_n);
+	assert_int_equal(2, curr_view->list_pos);
+	(void)vle_keys_exec_timed_out(WK_C_y);
+	assert_int_equal(2, curr_view->list_pos);
+	/* This triggers view cursor update. */
+	(void)vle_keys_exec_timed_out(L"a" WK_C_h);
+	assert_int_equal(2, curr_view->list_pos);
+	(void)vle_keys_exec_timed_out(WK_C_c);
+
+	/* Filter. */
+	curr_view->list_pos = 0;
+	(void)vle_keys_exec_timed_out(L"=");
+	/* Empty input. */
+	(void)vle_keys_exec_timed_out(WK_C_y);
+	assert_int_equal(0, curr_view->list_pos);
+	(void)vle_keys_exec_timed_out(WK_C_n);
+	assert_int_equal(1, curr_view->list_pos);
+	(void)vle_keys_exec_timed_out(WK_C_y);
+	assert_int_equal(1, curr_view->list_pos);
+	/* This triggers view cursor update. */
+	(void)vle_keys_exec_timed_out(L"a" WK_C_h);
+	assert_int_equal(1, curr_view->list_pos);
+	/* Non-empty input. */
+	(void)vle_keys_exec_timed_out(WK_C_y L".");
+	assert_int_equal(1, curr_view->list_pos);
+	(void)vle_keys_exec_timed_out(WK_C_n);
+	assert_int_equal(2, curr_view->list_pos);
+	(void)vle_keys_exec_timed_out(WK_C_y);
+	assert_int_equal(2, curr_view->list_pos);
+	/* This triggers view cursor update. */
+	(void)vle_keys_exec_timed_out(L"a" WK_C_h);
+	assert_int_equal(2, curr_view->list_pos);
+	(void)vle_keys_exec_timed_out(WK_C_c);
+
+	conf_teardown();
+}
+
+TEST(navigation_preserves_input_on_enter_failure, IF(regular_unix_user))
+{
+	conf_setup();
+	create_dir(SANDBOX_PATH "/dir");
+	assert_success(os_chmod(SANDBOX_PATH "/dir", 0000));
+
+	cfg.inc_search = 1;
+	make_abs_path(curr_view->curr_dir, sizeof(curr_view->curr_dir), SANDBOX_PATH,
+			"", NULL);
+	populate_dir_list(curr_view, /*reload=*/0);
+
+	(void)vle_keys_exec_timed_out(L"/" WK_C_y L"di" WK_C_m);
+	assert_wstring_equal(L"di", stats->line);
+	(void)vle_keys_exec_timed_out(WK_C_c);
+
+	remove_dir(SANDBOX_PATH "/dir");
+	conf_teardown();
+}
+
+TEST(navigation_opens_files, IF(not_windows))
+{
+	conf_setup();
+	cfg.inc_search = 1;
+	cfg.nav_open_files = 1;
+	stats_init(&cfg);
+
+	create_executable(SANDBOX_PATH "/script");
+	make_file(SANDBOX_PATH "/script",
+			"#!/bin/sh\n"
+			"touch " SANDBOX_PATH "/out");
+	create_file(SANDBOX_PATH "/in");
+
+	char vi_cmd[PATH_MAX + 1];
+	make_abs_path(vi_cmd, sizeof(vi_cmd), SANDBOX_PATH, "script", NULL);
+	update_string(&cfg.vi_command, vi_cmd);
+
+	make_abs_path(curr_view->curr_dir, sizeof(curr_view->curr_dir), SANDBOX_PATH,
+			"", NULL);
+	populate_dir_list(curr_view, /*reload=*/0);
+
+	/* This should create "out" file. */
+	(void)vle_keys_exec_timed_out(L"/" WK_C_y WK_C_m);
+
+	update_string(&cfg.vi_command, NULL);
+
+	remove_file(SANDBOX_PATH "/in");
+	remove_file(SANDBOX_PATH "/out");
+	remove_file(SANDBOX_PATH "/script");
+
+	conf_teardown();
+	cfg.nav_open_files = 0;
+}
+
+static void
+prompt_callback(const char response[], void *arg)
+{
+	update_string(&prompt_response, response);
+	++prompt_invocation_count;
+}
+
+static void
+column_line_print(const char buf[], size_t offset, AlignType align,
+		const char full_column[], const format_info_t *info)
+{
+	/* Do nothing. */
+}
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 : */
diff --git a/tests/misc/cmdline_completion.c b/tests/misc/cmdline_completion.c
index d6abdfa..fea4fdb 100644
--- a/tests/misc/cmdline_completion.c
+++ b/tests/misc/cmdline_completion.c
@@ -55,7 +55,7 @@ SETUP()
 
 	curr_view = &lwin;
 
-	init_commands();
+	cmds_init();
 
 	vle_cmds_run("command bar a");
 	vle_cmds_run("command baz b");
@@ -327,7 +327,7 @@ TEST(autocd_completion)
 	ASSERT_NEXT_MATCH("mydir3/");
 	ASSERT_NEXT_MATCH("myd");
 
-	ASSERT_COMPLETION(L"../m", L"../misc/");
+	ASSERT_COMPLETION(L"../m", L"../menus/");
 	ASSERT_COMPLETION(L"../misc/my", L"../misc/mydir1/");
 
 	cfg.auto_cd = 0;
diff --git a/tests/misc/cmdline_editing.c b/tests/misc/cmdline_editing.c
index 0de5899..ae5f6f7 100644
--- a/tests/misc/cmdline_editing.c
+++ b/tests/misc/cmdline_editing.c
@@ -22,7 +22,6 @@
 #include "../../src/utils/fs.h"
 #include "../../src/utils/matcher.h"
 #include "../../src/utils/str.h"
-#include "../../src/builtin_functions.h"
 #include "../../src/filelist.h"
 #include "../../src/status.h"
 
@@ -34,13 +33,12 @@ SETUP_ONCE()
 {
 	stats = get_line_stats();
 	try_enable_utf8_locale();
-	init_builtin_functions();
 }
 
 SETUP()
 {
-	init_modes();
-	modcline_enter(CLS_COMMAND, "", NULL);
+	modes_init();
+	modcline_enter(CLS_COMMAND, "");
 
 	curr_view = &lwin;
 	view_setup(&lwin);
@@ -175,6 +173,94 @@ TEST(left_kill)
 	assert_wstring_equal(L"", stats->line);
 }
 
+TEST(moving_cursor)
+{
+	(void)vle_keys_exec_timed_out(L"abc");
+	assert_wstring_equal(L"abc", stats->line);
+
+	(void)vle_keys_exec_timed_out(WK_C_b);
+	assert_int_equal(2, stats->index);
+	(void)vle_keys_exec_timed_out(WK_C_f);
+	assert_int_equal(3, stats->index);
+	(void)vle_keys_exec_timed_out(WK_C_a);
+	assert_int_equal(0, stats->index);
+	(void)vle_keys_exec_timed_out(WK_C_e);
+	assert_int_equal(3, stats->index);
+
+#ifdef ENABLE_EXTENDED_KEYS
+	wchar_t keys[2] = { };
+
+	keys[0] = K(KEY_LEFT);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_int_equal(2, stats->index);
+	keys[0] = K(KEY_RIGHT);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_int_equal(3, stats->index);
+	keys[0] = K(KEY_HOME);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_int_equal(0, stats->index);
+	keys[0] = K(KEY_END);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_int_equal(3, stats->index);
+#endif
+}
+
+TEST(word_operations)
+{
+	(void)vle_keys_exec_timed_out(L"aa bb cc");
+	assert_wstring_equal(L"aa bb cc", stats->line);
+
+	/* Fill cfg.word_chars as if it was initialized from isspace() function. */
+	memset(&cfg.word_chars, 1, sizeof(cfg.word_chars));
+	cfg.word_chars['\x00'] = 0; cfg.word_chars['\x09'] = 0;
+	cfg.word_chars['\x0a'] = 0; cfg.word_chars['\x0b'] = 0;
+	cfg.word_chars['\x0c'] = 0; cfg.word_chars['\x0d'] = 0;
+	cfg.word_chars['\x20'] = 0;
+
+#ifndef __PDCURSES__
+	(void)vle_keys_exec_timed_out(WK_ESC WK_b);
+	assert_int_equal(6, stats->index);
+	(void)vle_keys_exec_timed_out(WK_ESC WK_b);
+	assert_int_equal(3, stats->index);
+	(void)vle_keys_exec_timed_out(WK_ESC WK_b);
+	assert_int_equal(0, stats->index);
+	(void)vle_keys_exec_timed_out(WK_ESC WK_f);
+	assert_int_equal(2, stats->index);
+	(void)vle_keys_exec_timed_out(WK_ESC WK_f);
+	assert_int_equal(5, stats->index);
+	(void)vle_keys_exec_timed_out(WK_ESC WK_b);
+	assert_int_equal(3, stats->index);
+	(void)vle_keys_exec_timed_out(WK_ESC WK_d);
+	assert_int_equal(3, stats->index);
+	assert_wstring_equal(L"aa  cc", stats->line);
+#else
+	wchar_t keys[2] = { };
+
+	keys[0] = K(ALT_B);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_int_equal(6, stats->index);
+	keys[0] = K(ALT_B);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_int_equal(3, stats->index);
+	keys[0] = K(ALT_B);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_int_equal(0, stats->index);
+	keys[0] = K(ALT_F);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_int_equal(2, stats->index);
+	keys[0] = K(ALT_F);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_int_equal(5, stats->index);
+	keys[0] = K(ALT_B);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_int_equal(3, stats->index);
+	keys[0] = K(ALT_D);
+	(void)vle_keys_exec_timed_out(keys);
+	assert_int_equal(3, stats->index);
+	assert_wstring_equal(L"aa  cc", stats->line);
+#endif
+}
+
 TEST(history)
 {
 	cfg.history_len = 4;
@@ -445,26 +531,6 @@ TEST(expr_reg_bad_expr)
 	assert_wstring_equal(L"ad", stats->line);
 }
 
-/* This tests requires some interactivity and full command-line mode similar to
- * editing, hence it's here. */
-TEST(expr_reg_completion)
-{
-	(void)vle_keys_exec_timed_out(L":" WK_C_r WK_EQUALS);
-	(void)vle_keys_exec_timed_out(L"ex" WK_C_i);
-	assert_wstring_equal(L"executable(", stats->line);
-	(void)vle_keys_exec_timed_out(WK_C_c);
-}
-
-/* This tests requires some interactivity and full command-line mode similar to
- * editing, hence it's here. */
-TEST(expr_reg_completion_ignores_pipe)
-{
-	(void)vle_keys_exec_timed_out(L":" WK_C_r WK_EQUALS);
-	(void)vle_keys_exec_timed_out(L"ab|ex" WK_C_i);
-	assert_wstring_equal(L"ab|ex", stats->line);
-	(void)vle_keys_exec_timed_out(WK_C_c);
-}
-
 TEST(ext_edited_prompt_is_saved_to_history, IF(not_windows))
 {
 	FILE *fp;
diff --git a/tests/misc/cmdline_history.c b/tests/misc/cmdline_history.c
index 9d3a1fa..66e2760 100644
--- a/tests/misc/cmdline_history.c
+++ b/tests/misc/cmdline_history.c
@@ -41,7 +41,7 @@ SETUP()
 	update_string(&lwin.dir_entry[0].name, "fake");
 	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), SANDBOX_PATH, "", NULL);
 	conf_setup();
-	init_modes();
+	modes_init();
 	fops_init(&modcline_prompt, NULL);
 
 	stats.line = NULL;
diff --git a/tests/misc/command_separation.c b/tests/misc/command_separation.c
index ab179b9..2b6b9a6 100644
--- a/tests/misc/command_separation.c
+++ b/tests/misc/command_separation.c
@@ -9,7 +9,7 @@
 
 SETUP()
 {
-	init_commands();
+	cmds_init();
 }
 
 TEARDOWN()
diff --git a/tests/misc/compare.c b/tests/misc/compare.c
index cb484df..e8d89b9 100644
--- a/tests/misc/compare.c
+++ b/tests/misc/compare.c
@@ -41,7 +41,7 @@ TEARDOWN()
 TEST(files_are_compared_by_name)
 {
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/a");
-	compare_one_pane(&lwin, CT_NAME, LT_ALL, 0);
+	compare_one_pane(&lwin, CT_NAME, LT_ALL, CF_NONE);
 
 	assert_int_equal(CV_COMPARE, lwin.custom.type);
 	assert_int_equal(3, lwin.list_rows);
@@ -53,7 +53,7 @@ TEST(files_are_compared_by_name)
 TEST(files_are_compared_by_size)
 {
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/a");
-	compare_one_pane(&lwin, CT_SIZE, LT_ALL, 0);
+	compare_one_pane(&lwin, CT_SIZE, LT_ALL, CF_NONE);
 
 	assert_int_equal(CV_COMPARE, lwin.custom.type);
 	assert_int_equal(3, lwin.list_rows);
@@ -65,7 +65,7 @@ TEST(files_are_compared_by_size)
 TEST(files_are_compared_by_contents)
 {
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/b");
-	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, 0);
+	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, CF_NONE);
 
 	assert_int_equal(CV_COMPARE, lwin.custom.type);
 	assert_int_equal(4, lwin.list_rows);
@@ -75,11 +75,127 @@ TEST(files_are_compared_by_contents)
 	assert_int_equal(3, lwin.dir_entry[3].id);
 }
 
+TEST(two_panes_by_name_ignore_case)
+{
+	create_file(SANDBOX_PATH "/A");
+	create_file(SANDBOX_PATH "/Aa");
+	create_file(SANDBOX_PATH "/aAa");
+
+	strcpy(lwin.curr_dir, SANDBOX_PATH);
+	strcpy(rwin.curr_dir, TEST_DATA_PATH "/rename");
+
+	compare_two_panes(CT_NAME, LT_ALL, CF_SHOW | CF_GROUP_PATHS | CF_IGNORE_CASE);
+
+	check_compare_invariants(3);
+
+	assert_int_equal(1, lwin.dir_entry[0].id);
+	assert_int_equal(2, lwin.dir_entry[1].id);
+	assert_int_equal(3, lwin.dir_entry[2].id);
+
+	assert_string_equal("A", lwin.dir_entry[0].name);
+	assert_string_equal("a", rwin.dir_entry[0].name);
+	assert_string_equal("Aa", lwin.dir_entry[1].name);
+	assert_string_equal("aa", rwin.dir_entry[1].name);
+	assert_string_equal("aAa", lwin.dir_entry[2].name);
+	assert_string_equal("aaa", rwin.dir_entry[2].name);
+
+	remove_file(SANDBOX_PATH "/A");
+	remove_file(SANDBOX_PATH "/Aa");
+	remove_file(SANDBOX_PATH "/aAa");
+}
+
+TEST(two_panes_by_name_ignore_case_sorts_with_isort)
+{
+	create_dir(SANDBOX_PATH "/l");
+	create_file(SANDBOX_PATH "/l/a");
+	create_file(SANDBOX_PATH "/l/b");
+	create_dir(SANDBOX_PATH "/r");
+	create_file(SANDBOX_PATH "/r/a");
+	create_file(SANDBOX_PATH "/r/B");
+
+	strcpy(lwin.curr_dir, SANDBOX_PATH "/l");
+	strcpy(rwin.curr_dir, SANDBOX_PATH "/r");
+
+	compare_two_panes(CT_NAME, LT_ALL, CF_SHOW | CF_GROUP_PATHS | CF_IGNORE_CASE);
+
+	check_compare_invariants(2);
+
+	assert_int_equal(1, lwin.dir_entry[0].id);
+	assert_int_equal(2, lwin.dir_entry[1].id);
+
+	assert_string_equal("a", lwin.dir_entry[0].name);
+	assert_string_equal("a", rwin.dir_entry[0].name);
+	assert_string_equal("b", lwin.dir_entry[1].name);
+	assert_string_equal("B", rwin.dir_entry[1].name);
+
+	remove_file(SANDBOX_PATH "/l/a");
+	remove_file(SANDBOX_PATH "/l/b");
+	remove_dir(SANDBOX_PATH "/l");
+	remove_file(SANDBOX_PATH "/r/a");
+	remove_file(SANDBOX_PATH "/r/B");
+	remove_dir(SANDBOX_PATH "/r");
+}
+
+TEST(two_panes_by_name_respect_case)
+{
+	create_file(SANDBOX_PATH "/A");
+	create_file(SANDBOX_PATH "/Aa");
+	create_file(SANDBOX_PATH "/aAa");
+
+	strcpy(lwin.curr_dir, SANDBOX_PATH);
+	strcpy(rwin.curr_dir, TEST_DATA_PATH "/rename");
+
+	compare_two_panes(CT_NAME, LT_ALL,
+			CF_SHOW | CF_GROUP_PATHS | CF_RESPECT_CASE);
+
+	check_compare_invariants(6);
+
+	remove_file(SANDBOX_PATH "/A");
+	remove_file(SANDBOX_PATH "/Aa");
+	remove_file(SANDBOX_PATH "/aAa");
+}
+
+TEST(two_panes_by_name_respace_case_sorts_with_sort)
+{
+	create_dir(SANDBOX_PATH "/l");
+	create_file(SANDBOX_PATH "/l/a");
+	create_file(SANDBOX_PATH "/l/b");
+	create_dir(SANDBOX_PATH "/r");
+	create_file(SANDBOX_PATH "/r/a");
+	create_file(SANDBOX_PATH "/r/B");
+
+	strcpy(lwin.curr_dir, SANDBOX_PATH "/l");
+	strcpy(rwin.curr_dir, SANDBOX_PATH "/r");
+
+	compare_two_panes(CT_NAME, LT_ALL,
+			CF_SHOW | CF_GROUP_PATHS | CF_RESPECT_CASE);
+
+	check_compare_invariants(3);
+
+	assert_int_equal(3, lwin.dir_entry[0].id);
+	assert_int_equal(1, lwin.dir_entry[1].id);
+	assert_int_equal(2, lwin.dir_entry[2].id);
+
+	assert_string_equal("", lwin.dir_entry[0].name);
+	assert_string_equal("B", rwin.dir_entry[0].name);
+	assert_string_equal("a", lwin.dir_entry[1].name);
+	assert_string_equal("a", rwin.dir_entry[1].name);
+	assert_string_equal("b", lwin.dir_entry[2].name);
+	assert_string_equal("", rwin.dir_entry[2].name);
+
+	remove_file(SANDBOX_PATH "/l/a");
+	remove_file(SANDBOX_PATH "/l/b");
+	remove_dir(SANDBOX_PATH "/l");
+	remove_file(SANDBOX_PATH "/r/a");
+	remove_file(SANDBOX_PATH "/r/B");
+	remove_dir(SANDBOX_PATH "/r");
+}
+
 TEST(two_panes_all_group_ids)
 {
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/a");
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/b");
-	compare_two_panes(CT_NAME, LT_ALL, 0, 0);
+	compare_two_panes(CT_NAME, LT_ALL, CF_SHOW);
 
 	check_compare_invariants(4);
 
@@ -104,7 +220,7 @@ TEST(two_panes_all_group_paths)
 	other_view = &lwin;
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/a");
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/b");
-	compare_two_panes(CT_NAME, LT_ALL, 1, 0);
+	compare_two_panes(CT_NAME, LT_ALL, CF_GROUP_PATHS | CF_SHOW);
 
 	check_compare_invariants(4);
 
@@ -129,7 +245,7 @@ TEST(two_panes_dups_one_is_empty)
 	other_view = &lwin;
 	strcpy(lwin.curr_dir, SANDBOX_PATH);
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/b");
-	compare_two_panes(CT_CONTENTS, LT_ALL, 1, 0);
+	compare_two_panes(CT_CONTENTS, LT_ALL, CF_GROUP_PATHS | CF_SHOW);
 
 	check_compare_invariants(4);
 
@@ -152,7 +268,7 @@ TEST(two_panes_dups)
 {
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/a");
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/b");
-	compare_two_panes(CT_CONTENTS, LT_DUPS, 1, 0);
+	compare_two_panes(CT_CONTENTS, LT_DUPS, CF_GROUP_PATHS | CF_SHOW);
 
 	check_compare_invariants(3);
 
@@ -172,7 +288,7 @@ TEST(two_panes_unique)
 {
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/a");
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/b");
-	compare_two_panes(CT_CONTENTS, LT_UNIQUE, 1, 0);
+	compare_two_panes(CT_CONTENTS, LT_UNIQUE, CF_GROUP_PATHS | CF_SHOW);
 
 	assert_int_equal(1, lwin.list_rows);
 	assert_int_equal(1, rwin.list_rows);
@@ -189,7 +305,7 @@ TEST(single_pane_all)
 	copy_file(TEST_DATA_PATH "/read/utf8-bom", SANDBOX_PATH "/utf8-bom-2");
 
 	strcpy(lwin.curr_dir, SANDBOX_PATH);
-	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, 0);
+	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, CF_NONE);
 
 	assert_int_equal(CV_COMPARE, lwin.custom.type);
 	assert_int_equal(4, lwin.list_rows);
@@ -209,7 +325,7 @@ TEST(single_pane_all)
 TEST(single_pane_dups)
 {
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare");
-	compare_one_pane(&lwin, CT_CONTENTS, LT_DUPS, 0);
+	compare_one_pane(&lwin, CT_CONTENTS, LT_DUPS, CF_NONE);
 
 	assert_int_equal(CV_COMPARE, lwin.custom.type);
 	assert_int_equal(5, lwin.list_rows);
@@ -229,7 +345,7 @@ TEST(single_pane_unique)
 	copy_file(TEST_DATA_PATH "/read/utf8-bom", SANDBOX_PATH "/utf8-bom-2");
 
 	strcpy(lwin.curr_dir, SANDBOX_PATH);
-	compare_one_pane(&lwin, CT_CONTENTS, LT_UNIQUE, 0);
+	compare_one_pane(&lwin, CT_CONTENTS, LT_UNIQUE, CF_NONE);
 
 	assert_int_equal(CV_REGULAR, lwin.custom.type);
 	assert_int_equal(1, lwin.list_rows);
@@ -251,36 +367,51 @@ TEST(relatively_complex_match)
 	other_view = &lwin;
 	strcpy(lwin.curr_dir, SANDBOX_PATH);
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/read");
-	compare_two_panes(CT_CONTENTS, LT_ALL, 1, 0);
+	compare_two_panes(CT_CONTENTS, LT_ALL, CF_GROUP_PATHS | CF_SHOW);
 
-	check_compare_invariants(8);
+	check_compare_invariants(10);
 
 	assert_int_equal(1, lwin.dir_entry[0].id);
 	assert_int_equal(2, lwin.dir_entry[1].id);
-	assert_int_equal(3, lwin.dir_entry[2].id);
-	assert_int_equal(4, lwin.dir_entry[3].id);
-	assert_int_equal(2, lwin.dir_entry[4].id);
-	assert_int_equal(5, lwin.dir_entry[5].id);
-	assert_int_equal(6, lwin.dir_entry[6].id);
+	assert_int_equal(2, lwin.dir_entry[2].id);
+	assert_int_equal(2, lwin.dir_entry[3].id);
+	assert_int_equal(3, lwin.dir_entry[4].id);
+	assert_int_equal(4, lwin.dir_entry[5].id);
+	assert_int_equal(5, lwin.dir_entry[6].id);
 	assert_int_equal(5, lwin.dir_entry[7].id);
+	assert_int_equal(5, lwin.dir_entry[8].id);
+	assert_int_equal(6, lwin.dir_entry[9].id);
 
 	assert_string_equal("", lwin.dir_entry[0].name);
 	assert_string_equal("binary-data", rwin.dir_entry[0].name);
-	assert_string_equal("dos-eof-1", lwin.dir_entry[1].name);
+
+	assert_string_equal("", lwin.dir_entry[1].name);
 	assert_string_equal("dos-eof", rwin.dir_entry[1].name);
-	assert_string_equal("", lwin.dir_entry[2].name);
-	assert_string_equal("dos-line-endings", rwin.dir_entry[2].name);
-	assert_string_equal("", lwin.dir_entry[3].name);
-	assert_string_equal("two-lines", rwin.dir_entry[3].name);
-	assert_string_equal("dos-eof-2", lwin.dir_entry[4].name);
-	assert_string_equal("", rwin.dir_entry[4].name);
-	assert_string_equal("utf8-bom-1", lwin.dir_entry[5].name);
-	assert_string_equal("utf8-bom", rwin.dir_entry[5].name);
+
+	assert_string_equal("dos-eof-1", lwin.dir_entry[2].name);
+	assert_string_equal("", rwin.dir_entry[2].name);
+
+	assert_string_equal("dos-eof-2", lwin.dir_entry[3].name);
+	assert_string_equal("", rwin.dir_entry[3].name);
+
+	assert_string_equal("", lwin.dir_entry[4].name);
+	assert_string_equal("dos-line-endings", rwin.dir_entry[4].name);
+
+	assert_string_equal("", lwin.dir_entry[5].name);
+	assert_string_equal("two-lines", rwin.dir_entry[5].name);
+
 	assert_string_equal("", lwin.dir_entry[6].name);
-	assert_string_equal("very-long-line", rwin.dir_entry[6].name);
-	assert_string_equal("utf8-bom-2", lwin.dir_entry[7].name);
+	assert_string_equal("utf8-bom", rwin.dir_entry[6].name);
+
+	assert_string_equal("utf8-bom-1", lwin.dir_entry[7].name);
 	assert_string_equal("", rwin.dir_entry[7].name);
 
+	assert_string_equal("utf8-bom-2", lwin.dir_entry[8].name);
+	assert_string_equal("", rwin.dir_entry[8].name);
+
+	assert_string_equal("", lwin.dir_entry[9].name);
+	assert_string_equal("very-long-line", rwin.dir_entry[9].name);
+
 	assert_success(remove(SANDBOX_PATH "/dos-eof-1"));
 	assert_success(remove(SANDBOX_PATH "/dos-eof-2"));
 	assert_success(remove(SANDBOX_PATH "/utf8-bom-1"));
@@ -304,7 +435,7 @@ TEST(content_difference_is_detected)
 	other_view = &rwin;
 	strcpy(lwin.curr_dir, SANDBOX_PATH "/a");
 	strcpy(rwin.curr_dir, SANDBOX_PATH "/b");
-	compare_two_panes(CT_CONTENTS, LT_ALL, 1, 0);
+	compare_two_panes(CT_CONTENTS, LT_ALL, CF_GROUP_PATHS | CF_SHOW);
 
 	assert_int_equal(1, lwin.list_rows);
 	assert_int_equal(1, rwin.list_rows);
diff --git a/tests/misc/compare_misc.c b/tests/misc/compare_misc.c
index dde204f..24ae9fa 100644
--- a/tests/misc/compare_misc.c
+++ b/tests/misc/compare_misc.c
@@ -55,7 +55,7 @@ TEARDOWN()
 TEST(empty_root_directories_abort_single_comparison)
 {
 	strcpy(lwin.curr_dir, SANDBOX_PATH);
-	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, 0);
+	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, CF_NONE);
 
 	assert_false(flist_custom_active(&lwin));
 }
@@ -68,7 +68,7 @@ TEST(empty_root_directories_abort_dual_comparison)
 	strcpy(lwin.curr_dir, SANDBOX_PATH "/a");
 	strcpy(rwin.curr_dir, SANDBOX_PATH "/b");
 
-	compare_two_panes(CT_CONTENTS, LT_ALL, 0, 0);
+	compare_two_panes(CT_CONTENTS, LT_ALL, CF_SHOW);
 	assert_false(flist_custom_active(&lwin));
 
 	assert_success(rmdir(SANDBOX_PATH "/a"));
@@ -83,7 +83,7 @@ TEST(empty_unique_cv_are_created)
 	strcpy(lwin.curr_dir, SANDBOX_PATH "/a");
 	strcpy(rwin.curr_dir, SANDBOX_PATH "/b");
 
-	compare_two_panes(CT_CONTENTS, LT_UNIQUE, 0, 0);
+	compare_two_panes(CT_CONTENTS, LT_UNIQUE, CF_SHOW);
 	assert_true(flist_custom_active(&lwin));
 	assert_true(flist_custom_active(&rwin));
 
@@ -100,7 +100,7 @@ TEST(empty_unique_cv_are_created)
 TEST(listing_wrong_path_does_nothing)
 {
 	strcpy(lwin.curr_dir, SANDBOX_PATH "/does-not-exist");
-	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, 0);
+	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, CF_NONE);
 
 	assert_false(flist_custom_active(&lwin));
 }
@@ -108,7 +108,7 @@ TEST(listing_wrong_path_does_nothing)
 TEST(files_are_found_recursively)
 {
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/tree");
-	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, 0);
+	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, CF_NONE);
 
 	assert_int_equal(7, lwin.list_rows);
 }
@@ -121,7 +121,7 @@ TEST(compare_skips_dir_symlinks, IF(not_windows))
 #endif
 
 	strcpy(lwin.curr_dir, SANDBOX_PATH);
-	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, 0);
+	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, CF_NONE);
 	assert_false(flist_custom_active(&lwin));
 
 	assert_success(remove(SANDBOX_PATH "/link"));
@@ -130,13 +130,16 @@ TEST(compare_skips_dir_symlinks, IF(not_windows))
 TEST(not_available_files_are_ignored, IF(regular_unix_user))
 {
 	copy_file(TEST_DATA_PATH "/read/utf8-bom", SANDBOX_PATH "/utf8-bom");
+	copy_file(TEST_DATA_PATH "/read/utf8-bom", SANDBOX_PATH "/utf8-bom-2");
 	assert_success(chmod(SANDBOX_PATH "/utf8-bom", 0000));
+	assert_success(chmod(SANDBOX_PATH "/utf8-bom-2", 0000));
 
 	strcpy(lwin.curr_dir, SANDBOX_PATH);
-	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, 0);
+	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, CF_SKIP_EMPTY);
 	assert_false(flist_custom_active(&lwin));
 
 	assert_success(remove(SANDBOX_PATH "/utf8-bom"));
+	assert_success(remove(SANDBOX_PATH "/utf8-bom-2"));
 }
 
 TEST(two_panes_are_left_in_sync)
@@ -144,7 +147,7 @@ TEST(two_panes_are_left_in_sync)
 	strcpy(lwin.curr_dir, SANDBOX_PATH);
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/a");
 
-	compare_two_panes(CT_CONTENTS, LT_ALL, 0, 0);
+	compare_two_panes(CT_CONTENTS, LT_ALL, CF_SHOW);
 	assert_true(flist_custom_active(&lwin));
 	assert_true(flist_custom_active(&rwin));
 
@@ -166,7 +169,7 @@ TEST(exclude_works_with_entries_or_their_groups)
 
 	strcpy(lwin.curr_dir, SANDBOX_PATH);
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/a");
-	compare_two_panes(CT_CONTENTS, LT_ALL, 0, 0);
+	compare_two_panes(CT_CONTENTS, LT_ALL, CF_SHOW);
 	check_compare_invariants(5);
 
 	/* Does nothing on separator. */
@@ -217,12 +220,12 @@ TEST(local_filter_is_not_set)
 {
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/a");
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/b");
-	compare_two_panes(CT_NAME, LT_ALL, 0, 0);
+	compare_two_panes(CT_NAME, LT_ALL, CF_SHOW);
 
-	exec_command("f", &lwin, CIT_FILTER_PATTERN);
+	cmds_dispatch1("f", &lwin, CIT_FILTER_PATTERN);
 	assert_true(filter_is_empty(&lwin.local_filter.filter));
 
-	modcline_enter(CLS_FILTER, lwin.local_filter.filter.raw, NULL);
+	modcline_enter(CLS_FILTER, lwin.local_filter.filter.raw);
 	assert_true(vle_mode_is(NORMAL_MODE));
 }
 
@@ -239,7 +242,7 @@ TEST(removed_files_disappear_in_both_views_on_reload)
 
 	strcpy(lwin.curr_dir, SANDBOX_PATH);
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/a");
-	compare_two_panes(CT_CONTENTS, LT_ALL, 0, 0);
+	compare_two_panes(CT_CONTENTS, LT_ALL, CF_SHOW);
 	check_compare_invariants(5);
 
 	assert_success(remove(SANDBOX_PATH "/same-content-different-name-1"));
@@ -268,7 +271,7 @@ TEST(comparison_views_are_closed_when_no_files_are_left)
 
 	strcpy(lwin.curr_dir, SANDBOX_PATH "/a");
 	strcpy(rwin.curr_dir, SANDBOX_PATH "/b");
-	compare_two_panes(CT_CONTENTS, LT_ALL, 0, 0);
+	compare_two_panes(CT_CONTENTS, LT_ALL, CF_SHOW);
 	check_compare_invariants(1);
 
 	assert_success(remove(SANDBOX_PATH "/a/same-content-different-name-1"));
@@ -291,13 +294,13 @@ TEST(comparison_views_are_closed_when_no_files_are_left)
 TEST(sorting_is_not_changed)
 {
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/a");
-	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, 0);
+	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, CF_NONE);
 
 	change_sort_type(&lwin, SK_BY_SIZE, 0);
 	assert_int_equal(SK_NONE, lwin.sort[0]);
 
-	init_commands();
-	assert_success(exec_commands("set sort=ext", &lwin, CIT_COMMAND));
+	cmds_init();
+	assert_success(cmds_dispatch1("set sort=ext", &lwin, CIT_COMMAND));
 	assert_int_equal(SK_NONE, lwin.sort[0]);
 	vle_cmds_reset();
 }
@@ -306,7 +309,7 @@ TEST(cursor_moves_in_both_views_synchronously)
 {
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/a");
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/b");
-	compare_two_panes(CT_NAME, LT_ALL, 0, 0);
+	compare_two_panes(CT_NAME, LT_ALL, CF_SHOW);
 
 	assert_int_equal(0, lwin.list_pos);
 	assert_int_equal(lwin.list_pos, rwin.list_pos);
@@ -324,8 +327,8 @@ TEST(two_independent_compare_views_are_not_bound)
 {
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/a");
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/b");
-	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, 0);
-	compare_one_pane(&rwin, CT_CONTENTS, LT_ALL, 0);
+	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, CF_NONE);
+	compare_one_pane(&rwin, CT_CONTENTS, LT_ALL, CF_NONE);
 
 	assert_int_equal(0, lwin.list_pos);
 	assert_int_equal(lwin.list_pos, rwin.list_pos);
@@ -343,12 +346,12 @@ TEST(diff_is_closed_by_single_compare)
 {
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/a");
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/b");
-	compare_two_panes(CT_NAME, LT_ALL, 0, 0);
+	compare_two_panes(CT_NAME, LT_ALL, CF_SHOW);
 
 	assert_int_equal(CV_DIFF, lwin.custom.type);
 	assert_int_equal(CV_DIFF, rwin.custom.type);
 
-	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, 0);
+	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, CF_NONE);
 	assert_int_equal(CV_COMPARE, lwin.custom.type);
 	assert_int_equal(CV_REGULAR, rwin.custom.type);
 }
@@ -359,7 +362,7 @@ TEST(filtering_fake_entry_does_nothing)
 	other_view = &lwin;
 	strcpy(lwin.curr_dir, SANDBOX_PATH);
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/b");
-	compare_two_panes(CT_CONTENTS, LT_ALL, 1, 0);
+	compare_two_panes(CT_CONTENTS, LT_ALL, CF_GROUP_PATHS | CF_SHOW);
 
 	assert_int_equal(4, lwin.list_rows);
 	assert_int_equal(4, rwin.list_rows);
@@ -381,7 +384,7 @@ TEST(filtering_updates_two_bound_views)
 	other_view = &lwin;
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/a");
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/b");
-	compare_two_panes(CT_CONTENTS, LT_ALL, 0, 0);
+	compare_two_panes(CT_CONTENTS, LT_ALL, CF_SHOW);
 
 	/* Check that single file is excluded. */
 
@@ -427,7 +430,7 @@ TEST(two_pane_dups_renumbering)
 	other_view = &lwin;
 	strcpy(lwin.curr_dir, SANDBOX_PATH);
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/read");
-	compare_two_panes(CT_CONTENTS, LT_DUPS, 1, 0);
+	compare_two_panes(CT_CONTENTS, LT_DUPS, CF_SHOW);
 
 	check_compare_invariants(2);
 
@@ -446,7 +449,7 @@ TEST(removing_all_files_of_same_id_and_fake_entry_on_the_other_side)
 
 	strcpy(lwin.curr_dir, SANDBOX_PATH);
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/read");
-	compare_two_panes(CT_CONTENTS, LT_ALL, 1, 0);
+	compare_two_panes(CT_CONTENTS, LT_ALL, CF_SHOW);
 
 	check_compare_invariants(7);
 
@@ -471,9 +474,9 @@ TEST(compare_considers_dot_filter)
 	rwin.hide_dot = 1;
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/tree");
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/tree/dir5");
-	compare_two_panes(CT_CONTENTS, LT_ALL, 1, 0);
-	assert_int_equal(5, lwin.list_rows);
-	assert_int_equal(5, rwin.list_rows);
+	compare_two_panes(CT_CONTENTS, LT_ALL, CF_GROUP_PATHS | CF_SHOW);
+	assert_int_equal(6, lwin.list_rows);
+	assert_int_equal(6, rwin.list_rows);
 }
 
 TEST(compare_considers_name_filters)
@@ -488,11 +491,11 @@ TEST(compare_considers_name_filters)
 	name_filters_add_active(&lwin);
 	assert_int_equal(2, lwin.list_rows);
 
-	exec_command("different-content", &rwin, CIT_FILTER_PATTERN);
+	cmds_dispatch1("different-content", &rwin, CIT_FILTER_PATTERN);
 	load_dir_list(&rwin, 1);
 	assert_int_equal(1, rwin.list_rows);
 
-	compare_two_panes(CT_CONTENTS, LT_ALL, /*group_paths=*/0, /*skip_empty=*/0);
+	compare_two_panes(CT_CONTENTS, LT_ALL, CF_SHOW);
 
 	check_compare_invariants(3);
 
@@ -509,7 +512,7 @@ TEST(empty_files_are_skipped_if_requested)
 {
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/a");
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/b");
-	compare_two_panes(CT_CONTENTS, LT_ALL, 0, 1);
+	compare_two_panes(CT_CONTENTS, LT_ALL, CF_SKIP_EMPTY | CF_SHOW);
 	check_compare_invariants(4);
 }
 
@@ -517,7 +520,8 @@ TEST(custom_views_are_compared)
 {
 	char path[PATH_MAX + 1];
 
-	strcpy(lwin.curr_dir, "no-such-path");
+	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), TEST_DATA_PATH,
+			"/compare/a", saved_cwd);
 	flist_custom_start(&lwin, "test");
 	make_abs_path(path, sizeof(path), TEST_DATA_PATH,
 			"compare/a/same-content-different-name-1", saved_cwd);
@@ -532,7 +536,7 @@ TEST(custom_views_are_compared)
 
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/b");
 
-	compare_two_panes(CT_NAME, LT_ALL, 1, 0);
+	compare_two_panes(CT_NAME, LT_ALL, CF_GROUP_PATHS | CF_SHOW);
 
 	check_compare_invariants(4);
 
@@ -561,7 +565,7 @@ TEST(directories_are_not_added_from_custom_views)
 
 	strcpy(rwin.curr_dir, SANDBOX_PATH);
 
-	compare_two_panes(CT_NAME, LT_ALL, 1, 0);
+	compare_two_panes(CT_NAME, LT_ALL, CF_GROUP_PATHS | CF_SHOW);
 
 	check_compare_invariants(1);
 
@@ -574,7 +578,7 @@ TEST(the_same_directories_are_not_compared)
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare");
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare");
 
-	compare_two_panes(CT_CONTENTS, LT_ALL, 0, 0);
+	compare_two_panes(CT_CONTENTS, LT_ALL, CF_SHOW);
 	assert_false(flist_custom_active(&lwin));
 	assert_false(flist_custom_active(&rwin));
 }
@@ -595,7 +599,7 @@ TEST(two_panes_unique_is_symmetric)
 
 	curr_view = &lwin;
 	other_view = &rwin;
-	compare_two_panes(CT_NAME, LT_UNIQUE, /*group_paths=*/0, /*skip_empty=*/0);
+	compare_two_panes(CT_NAME, LT_UNIQUE, CF_SHOW);
 	assert_int_equal(1, lwin.list_rows);
 	assert_int_equal(1, rwin.list_rows);
 	assert_string_equal("fileb", lwin.dir_entry[0].name);
@@ -606,7 +610,7 @@ TEST(two_panes_unique_is_symmetric)
 
 	curr_view = &rwin;
 	other_view = &lwin;
-	compare_two_panes(CT_NAME, LT_UNIQUE, /*group_paths=*/0, /*skip_empty=*/0);
+	compare_two_panes(CT_NAME, LT_UNIQUE, CF_SHOW);
 	assert_int_equal(1, lwin.list_rows);
 	assert_int_equal(1, rwin.list_rows);
 	assert_string_equal("fileb", lwin.dir_entry[0].name);
diff --git a/tests/misc/count_vars.c b/tests/misc/count_vars.c
index a97807b..31949b8 100644
--- a/tests/misc/count_vars.c
+++ b/tests/misc/count_vars.c
@@ -18,8 +18,8 @@ SETUP()
 	curr_view = &lwin;
 
 	view_setup(&lwin);
-	init_commands();
-	init_modes();
+	cmds_init();
+	modes_init();
 	opt_handlers_setup();
 }
 
diff --git a/tests/misc/diff.c b/tests/misc/diff.c
index 029ad74..417fcc7 100644
--- a/tests/misc/diff.c
+++ b/tests/misc/diff.c
@@ -10,12 +10,18 @@
 #include <test-utils.h>
 
 #include "../../src/cfg/config.h"
+#include "../../src/engine/keys.h"
+#include "../../src/modes/modes.h"
+#include "../../src/modes/wk.h"
 #include "../../src/ui/column_view.h"
+#include "../../src/ui/statusbar.h"
 #include "../../src/ui/ui.h"
 #include "../../src/utils/fs.h"
 #include "../../src/compare.h"
 #include "../../src/event_loop.h"
+#include "../../src/flist_sel.h"
 #include "../../src/ops.h"
+#include "../../src/status.h"
 
 static void column_line_print(const char buf[], size_t offset, AlignType align,
 		const char full_column[], const format_info_t *info);
@@ -31,6 +37,7 @@ SETUP()
 	view_setup(&lwin);
 	view_setup(&rwin);
 
+	modes_init();
 	opt_handlers_setup();
 
 	cfg.use_system_calls = 1;
@@ -48,6 +55,7 @@ TEARDOWN()
 	view_teardown(&lwin);
 	view_teardown(&rwin);
 
+	vle_keys_reset();
 	opt_handlers_teardown();
 
 	undo_teardown();
@@ -57,7 +65,7 @@ TEST(moving_does_not_work_in_non_diff)
 {
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/a");
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/b");
-	(void)compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, 0);
+	(void)compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, CF_NONE);
 
 	(void)compare_move(&lwin, &rwin);
 
@@ -73,9 +81,9 @@ TEST(moving_fake_entry_removes_the_other_file)
 
 	create_file(SANDBOX_PATH "/empty");
 
-	(void)compare_two_panes(CT_CONTENTS, LT_ALL, 1, 0);
-	rwin.list_pos = 4;
-	lwin.list_pos = 4;
+	(void)compare_two_panes(CT_CONTENTS, LT_ALL, CF_GROUP_PATHS | CF_SHOW);
+	rwin.list_pos = 0;
+	lwin.list_pos = 0;
 	(void)compare_move(&lwin, &rwin);
 
 	assert_failure(remove(SANDBOX_PATH "/empty"));
@@ -88,7 +96,7 @@ TEST(moving_to_fake_entry_creates_the_other_file_and_entry_is_updated)
 	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), TEST_DATA_PATH,
 			"compare/b", saved_cwd);
 
-	(void)compare_two_panes(CT_CONTENTS, LT_ALL, 1, 0);
+	(void)compare_two_panes(CT_CONTENTS, LT_ALL, CF_GROUP_PATHS | CF_SHOW);
 	rwin.list_pos = 3;
 	lwin.list_pos = 3;
 	(void)compare_move(&lwin, &rwin);
@@ -106,10 +114,6 @@ TEST(moving_to_fake_entry_creates_the_other_file_and_entry_is_updated)
 	assert_true(process_scheduled_updates_of_view(&rwin));
 
 	curr_stats.load_stage = 0;
-	columns_free(lwin.columns);
-	lwin.columns = NULL;
-	columns_free(rwin.columns);
-	rwin.columns = NULL;
 	columns_teardown();
 
 	assert_true(lwin.dir_entry[3].id == rwin.dir_entry[3].id);
@@ -139,7 +143,7 @@ TEST(moving_mismatched_entry_makes_files_equal)
 	assert_false(files_are_identical(SANDBOX_PATH "/same-name-different-content",
 				TEST_DATA_PATH "/compare/b/same-name-different-content"));
 
-	(void)compare_two_panes(CT_CONTENTS, LT_ALL, 1, 0);
+	(void)compare_two_panes(CT_CONTENTS, LT_ALL, CF_GROUP_PATHS | CF_SHOW);
 	rwin.list_pos = 2;
 	lwin.list_pos = 2;
 	(void)compare_move(&lwin, &rwin);
@@ -161,7 +165,7 @@ TEST(moving_equal_does_nothing)
 	assert_true(files_are_identical(SANDBOX_PATH "/same-name-same-content",
 				TEST_DATA_PATH "/compare/b/same-name-same-content"));
 
-	(void)compare_two_panes(CT_CONTENTS, LT_ALL, 1, 0);
+	(void)compare_two_panes(CT_CONTENTS, LT_ALL, CF_GROUP_PATHS | CF_SHOW);
 
 	/* Replace file unbeknownst to main code. */
 	copy_file(TEST_DATA_PATH "/compare/a/same-name-different-content",
@@ -180,6 +184,55 @@ TEST(moving_equal_does_nothing)
 	assert_success(remove(SANDBOX_PATH "/same-name-same-content"));
 }
 
+TEST(can_move_selection)
+{
+	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/a");
+	strcpy(rwin.curr_dir, SANDBOX_PATH);
+
+	(void)compare_two_panes(CT_CONTENTS, LT_ALL, CF_GROUP_PATHS | CF_SHOW);
+	flist_sel_count(&lwin, 0, lwin.list_rows);
+	(void)compare_move(&lwin, &rwin);
+	assert_int_equal(0, lwin.selected_files);
+
+	remove_file(SANDBOX_PATH "/same-content-different-name-1");
+	remove_file(SANDBOX_PATH "/same-name-different-content");
+	remove_file(SANDBOX_PATH "/same-name-same-content");
+}
+
+TEST(diff_stats_are_correct_and_stay_correct)
+{
+	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/a");
+	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/b");
+
+	ui_sb_msg("");
+	(void)compare_two_panes(CT_CONTENTS, LT_ALL,
+			CF_SHOW_UNIQUE_LEFT | CF_SHOW_IDENTICAL);
+	modes_statusbar_update();
+	assert_string_equal("(on compare) +identical: 2, +/-unique: 1/2",
+			ui_sb_last());
+	(void)vle_keys_exec_timed_out(WK_C_w WK_x);
+	curr_stats.save_msg = 0;
+	modes_statusbar_update();
+	assert_string_equal("(on compare) +identical: 2, -/+unique: 2/1",
+			ui_sb_last());
+
+	/* Repeat the same, but with grouping by paths. */
+	ui_sb_msg("");
+	(void)compare_two_panes(CT_CONTENTS, LT_ALL,
+			CF_SHOW_UNIQUE_LEFT | CF_SHOW_IDENTICAL | CF_GROUP_PATHS);
+	curr_stats.save_msg = 0;
+	modes_statusbar_update();
+	assert_string_equal(
+			"(on compare) +identical: 2, -different: 1, +/-unique: 1/0",
+			ui_sb_last());
+	(void)vle_keys_exec_timed_out(WK_C_w WK_x);
+	curr_stats.save_msg = 0;
+	modes_statusbar_update();
+	assert_string_equal(
+			"(on compare) +identical: 2, -different: 1, -/+unique: 0/1",
+			ui_sb_last());
+}
+
 TEST(file_id_is_not_updated_on_failed_move, IF(regular_unix_user))
 {
 	make_abs_path(rwin.curr_dir, sizeof(rwin.curr_dir), SANDBOX_PATH, "",
@@ -193,7 +246,7 @@ TEST(file_id_is_not_updated_on_failed_move, IF(regular_unix_user))
 	assert_false(files_are_identical(SANDBOX_PATH "/same-name-different-content",
 				TEST_DATA_PATH "/compare/b/same-name-different-content"));
 
-	(void)compare_two_panes(CT_CONTENTS, LT_ALL, 1, 0);
+	(void)compare_two_panes(CT_CONTENTS, LT_ALL, CF_GROUP_PATHS | CF_SHOW);
 	rwin.list_pos = 2;
 	lwin.list_pos = 2;
 
diff --git a/tests/misc/dir_stack.c b/tests/misc/dir_stack.c
index 6dbeb69..abc18e2 100644
--- a/tests/misc/dir_stack.c
+++ b/tests/misc/dir_stack.c
@@ -9,7 +9,6 @@
 #include "../../src/ui/ui.h"
 #include "../../src/utils/fs.h"
 #include "../../src/utils/path.h"
-#include "../../src/utils/str.h"
 #include "../../src/utils/string_array.h"
 #include "../../src/dir_stack.h"
 #include "../../src/filelist.h"
@@ -75,13 +74,13 @@ TEST(swap_swaps_current_locations_and_stack_top)
 
 	assert_success(dir_stack_swap());
 
-	assert_true(ends_with(lwin.curr_dir, "/read"));
-	assert_true(ends_with(rwin.curr_dir, "/rename"));
+	assert_string_ends_with("/read", lwin.curr_dir);
+	assert_string_ends_with("/rename", rwin.curr_dir);
 
 	assert_success(dir_stack_swap());
 
-	assert_true(ends_with(lwin.curr_dir, "/rename"));
-	assert_true(ends_with(rwin.curr_dir, "/read"));
+	assert_string_ends_with("/rename", lwin.curr_dir);
+	assert_string_ends_with("/read", rwin.curr_dir);
 }
 
 TEST(empty_stack_is_described_via_current_directories)
@@ -109,11 +108,11 @@ TEST(non_empty_stack_is_described_correctly)
 	assert_string_equal("/new-left", list[0]);
 	assert_string_equal("/new-right", list[1]);
 	assert_string_equal("-----", list[2]);
-	assert_true(ends_with(list[3], "/rename"));
-	assert_true(ends_with(list[4], "/read"));
+	assert_string_ends_with("/rename", list[3]);
+	assert_string_ends_with("/read", list[4]);
 	assert_string_equal("-----", list[5]);
-	assert_true(ends_with(list[6], "/read"));
-	assert_true(ends_with(list[7], "/rename"));
+	assert_string_ends_with("/read", list[6]);
+	assert_string_ends_with("/rename", list[7]);
 	assert_string_equal(NULL, list[8]);
 	free_string_array(list, 9);
 }
@@ -154,8 +153,8 @@ TEST(rotate_does_nothing_for_zero_argument)
 
 	assert_success(dir_stack_rotate(0));
 
-	assert_true(ends_with(lwin.curr_dir, "/rename"));
-	assert_true(ends_with(rwin.curr_dir, "/read"));
+	assert_string_ends_with("/rename", lwin.curr_dir);
+	assert_string_ends_with("/read", rwin.curr_dir);
 }
 
 TEST(rotate_rotates_the_stack)
@@ -167,20 +166,20 @@ TEST(rotate_rotates_the_stack)
 	load_view_pair(TEST_DATA_PATH "/rename", TEST_DATA_PATH "/read");
 
 	assert_success(dir_stack_rotate(2));
-	assert_true(ends_with(lwin.curr_dir, "/read"));
-	assert_true(ends_with(rwin.curr_dir, "/rename"));
+	assert_string_ends_with("/read", lwin.curr_dir);
+	assert_string_ends_with("/rename", rwin.curr_dir);
 
 	assert_success(dir_stack_rotate(2));
-	assert_true(ends_with(lwin.curr_dir, "/tree"));
-	assert_true(ends_with(rwin.curr_dir, "/various-sizes"));
+	assert_string_ends_with("/tree", lwin.curr_dir);
+	assert_string_ends_with("/various-sizes", rwin.curr_dir);
 
 	assert_success(dir_stack_rotate(2));
-	assert_true(ends_with(lwin.curr_dir, "/rename"));
-	assert_true(ends_with(rwin.curr_dir, "/read"));
+	assert_string_ends_with("/rename", lwin.curr_dir);
+	assert_string_ends_with("/read", rwin.curr_dir);
 
 	assert_success(dir_stack_rotate(1));
-	assert_true(ends_with(lwin.curr_dir, "/tree"));
-	assert_true(ends_with(rwin.curr_dir, "/various-sizes"));
+	assert_string_ends_with("/tree", lwin.curr_dir);
+	assert_string_ends_with("/various-sizes", rwin.curr_dir);
 }
 
 static void
diff --git a/tests/misc/eval_arglist.c b/tests/misc/eval_arglist.c
index 29b7a36..342188a 100644
--- a/tests/misc/eval_arglist.c
+++ b/tests/misc/eval_arglist.c
@@ -18,7 +18,7 @@ SETUP_ONCE()
 	};
 	assert_success(function_register(&echo_function));
 
-	init_parser(&env_get);
+	vle_parser_init(&env_get);
 }
 
 static var_t
@@ -33,7 +33,7 @@ TEST(broken_syntax)
 	const char *stop_ptr;
 	char *result;
 
-	result = eval_arglist(args, &stop_ptr);
+	result = cmds_eval_args(args, &stop_ptr);
 	assert_string_equal(NULL, result);
 }
 
@@ -43,7 +43,7 @@ TEST(one_arg)
 	const char *stop_ptr;
 	char *result;
 
-	result = eval_arglist(args, &stop_ptr);
+	result = cmds_eval_args(args, &stop_ptr);
 	assert_true(result != NULL);
 	assert_string_equal("a", result);
 	free(result);
@@ -55,7 +55,7 @@ TEST(two_space_separated_args)
 	const char *stop_ptr;
 	char *result;
 
-	result = eval_arglist(args, &stop_ptr);
+	result = cmds_eval_args(args, &stop_ptr);
 	assert_true(result != NULL);
 	assert_string_equal("a b", result);
 	free(result);
@@ -67,7 +67,7 @@ TEST(two_dot_separated_args)
 	const char *stop_ptr;
 	char *result;
 
-	result = eval_arglist(args, &stop_ptr);
+	result = cmds_eval_args(args, &stop_ptr);
 	assert_true(result != NULL);
 	assert_string_equal("ab", result);
 	free(result);
@@ -79,7 +79,7 @@ TEST(double_single_quote)
 	const char *stop_ptr;
 	char *result;
 
-	result = eval_arglist(args, &stop_ptr);
+	result = cmds_eval_args(args, &stop_ptr);
 	assert_true(result != NULL);
 	assert_string_equal("a'b", result);
 	free(result);
@@ -91,7 +91,7 @@ TEST(wrong_expression_position)
 	const char *stop_ptr;
 	char *result;
 
-	result = eval_arglist(args, &stop_ptr);
+	result = cmds_eval_args(args, &stop_ptr);
 	assert_true(result == NULL);
 	assert_true(args + 4 == stop_ptr);
 	free(result);
@@ -103,7 +103,7 @@ TEST(empty_parens_fail)
 	const char *stop_ptr;
 	char *result;
 
-	result = eval_arglist(args, &stop_ptr);
+	result = cmds_eval_args(args, &stop_ptr);
 	assert_true(result == NULL);
 	free(result);
 }
@@ -114,7 +114,7 @@ TEST(chars_after_function_call_fail)
 	const char *stop_ptr;
 	char *result;
 
-	result = eval_arglist(args, &stop_ptr);
+	result = cmds_eval_args(args, &stop_ptr);
 	assert_true(result == NULL);
 	free(result);
 }
@@ -125,7 +125,7 @@ TEST(expression)
 	const char *stop_ptr;
 	char *result;
 
-	result = eval_arglist(args, &stop_ptr);
+	result = cmds_eval_args(args, &stop_ptr);
 	assert_true(result != NULL);
 	assert_string_equal("1", result);
 	free(result);
@@ -137,7 +137,7 @@ TEST(expression_and_literal)
 	const char *stop_ptr;
 	char *result;
 
-	result = eval_arglist(args, &stop_ptr);
+	result = cmds_eval_args(args, &stop_ptr);
 	assert_true(result != NULL);
 	assert_string_equal("1 b", result);
 	free(result);
@@ -149,7 +149,7 @@ TEST(call_function)
 	const char *stop_ptr;
 	char *result;
 
-	result = eval_arglist(args, &stop_ptr);
+	result = cmds_eval_args(args, &stop_ptr);
 	assert_true(result != NULL);
 	assert_string_equal("hello", result);
 	free(result);
@@ -161,7 +161,7 @@ TEST(broken_comparison_operator)
 	const char *stop_ptr;
 	char *result;
 
-	result = eval_arglist(args, &stop_ptr);
+	result = cmds_eval_args(args, &stop_ptr);
 	assert_true(result == NULL);
 	free(result);
 }
@@ -172,7 +172,7 @@ TEST(and_operator)
 	const char *stop_ptr;
 	char *result;
 
-	result = eval_arglist(args, &stop_ptr);
+	result = cmds_eval_args(args, &stop_ptr);
 	assert_string_equal("0", result);
 	free(result);
 }
diff --git a/tests/misc/event_loop.c b/tests/misc/event_loop.c
index 7ade657..bd8e046 100644
--- a/tests/misc/event_loop.c
+++ b/tests/misc/event_loop.c
@@ -5,11 +5,13 @@
 #include "../../src/cfg/config.h"
 #include "../../src/engine/cmds.h"
 #include "../../src/engine/keys.h"
+#include "../../src/lua/vlua.h"
 #include "../../src/modes/modes.h"
 #include "../../src/modes/wk.h"
 #include "../../src/ui/ui.h"
 #include "../../src/cmd_core.h"
 #include "../../src/event_loop.h"
+#include "../../src/status.h"
 
 static void x_key(key_info_t key_info, keys_info_t *keys_info);
 static void set_pending_key(key_info_t key_info, keys_info_t *keys_info);
@@ -25,8 +27,8 @@ SETUP()
 	view_setup(&lwin);
 	view_setup(&rwin);
 
-	init_modes();
-	init_commands();
+	modes_init();
+	cmds_init();
 
 	cfg.timeout_len = 1;
 }
@@ -58,6 +60,9 @@ TEST(quit_on_key_press)
 
 TEST(pending_flags_are_reset)
 {
+	/* This is just to test more parts of the loop. */
+	curr_stats.vlua = vlua_init();
+
 	keys_add_info_t x_key = { WK_x, { {&set_pending_key} } };
 	vle_keys_add(&x_key, 1U, NORMAL_MODE);
 
@@ -71,6 +76,9 @@ TEST(pending_flags_are_reset)
 
 	lwin.pending_marking = 0;
 	rwin.pending_marking = 0;
+
+	vlua_finish(curr_stats.vlua);
+	curr_stats.vlua = NULL;
 }
 
 static void
diff --git a/tests/misc/expand_custom_macros.c b/tests/misc/expand_custom_macros.c
index 3557e2a..ba84462 100644
--- a/tests/misc/expand_custom_macros.c
+++ b/tests/misc/expand_custom_macros.c
@@ -240,7 +240,7 @@ TEST(optional_unmatched)
 {
 	check_hi("%[%1*a", /*nmacros=*/0, /*macros=*/NULL, MA_OPT,
 			"%[a",
-			"  1");
+			"  b");
 }
 
 TEST(optional_with_empty_value)
@@ -371,9 +371,9 @@ TEST(highlighting_is_set_correctly)
 	custom_macro_t macros[] = {
 		{ .letter = 'i', .value = "xyz", .uses_left = 0, .group = -1, },
 	};
-	check_hi("prefix %1*%i%* suffix", ARRAY_LEN(macros), macros, MA_NOOPT,
+	check_hi("prefix%1* %10*%i%* %20*suffix", ARRAY_LEN(macros), macros, MA_NOOPT,
 			"prefix xyz suffix",
-			"       1  0      ");
+			"      bk  au     ");
 }
 
 TEST(opt_can_alter_highlighting_only)
@@ -383,20 +383,20 @@ TEST(opt_can_alter_highlighting_only)
 	};
 	check_hi("%1*%[%2*%C%]!", ARRAY_LEN(macros), macros, MA_OPT,
 			"!",
-			"1");
+			"b");
 
 	macros[0].value = "*";
 	check_hi("%1*%[%2*%C%]!", ARRAY_LEN(macros), macros, MA_OPT,
 			"!",
-			"2");
+			"c");
 }
 
 TEST(bad_user_group_remains_in_line_partially)
 {
 	custom_macro_t macros[1];
-	check_hi("%10*", 0, macros, MA_NOOPT,
-			"0*",
-			"  ");
+	check_hi("%100*", 0, macros, MA_NOOPT,
+			"00*",
+			"   ");
 }
 
 TEST(empty_optional_drops_attrs)
@@ -415,15 +415,15 @@ TEST(non_empty_optional_preserves_attrs)
 
 	check_hi("%1*%[%t%2*%t%]%3*", ARRAY_LEN(macros), macros, MA_OPT,
 			"filefile",
-			"1   2   ");
+			"b   c   ");
 
 	check_hi("%1*%[%2*%t%3*%t%]%4*", ARRAY_LEN(macros), macros, MA_OPT,
 			"filefile",
-			"2   3   ");
+			"c   d   ");
 
 	check_hi("%1*%[%2*%t%3*%]%4*", ARRAY_LEN(macros), macros, MA_OPT,
 			"file",
-			"2   ");
+			"c   ");
 }
 
 TEST(wide_characters_do_not_break_highlighting, IF(utf8_locale))
@@ -431,7 +431,7 @@ TEST(wide_characters_do_not_break_highlighting, IF(utf8_locale))
 	custom_macro_t macros[1];
 	check_hi("%1*螺丝 %= 螺%2*丝", 0, macros, MA_NOOPT,
 			"螺丝  螺丝",
-			"1       2 ");
+			"b       c ");
 }
 
 static void
diff --git a/tests/misc/expand_macros.c b/tests/misc/expand_macros.c
index 9274c2e..0e1a50e 100644
--- a/tests/misc/expand_macros.c
+++ b/tests/misc/expand_macros.c
@@ -308,7 +308,7 @@ TEST(good_flag_macros)
 
 	expanded = ma_expand("%N echo log", "", &flags, MER_OP);
 	assert_string_equal(" echo log", expanded);
-	assert_int_equal(MF_KEEP_SESSION, flags);
+	assert_int_equal(MF_KEEP_IN_FG, flags);
 	free(expanded);
 
 	expanded = ma_expand("%Pl echo log", "", &flags, MER_OP);
@@ -640,7 +640,7 @@ TEST(dollar_and_backtick_are_escaped_in_dquotes)
 	lwin.list_pos = 3;
 	assert_success(replace_string(&lwin.dir_entry[lwin.list_pos].name, "a$`b"));
 
-	curr_stats.shell_type = ST_NORMAL;
+	curr_stats.shell_type = ST_POSIX;
 	expanded = ma_expand("%\"c", "", NULL, MER_OP);
 	assert_string_equal("\"a\\$\\`b\"", expanded);
 	free(expanded);
@@ -651,7 +651,7 @@ TEST(dollar_and_backtick_are_escaped_in_dquotes)
 	free(expanded);
 
 	/* Restore normal value or some further tests can get broken. */
-	curr_stats.shell_type = ST_NORMAL;
+	curr_stats.shell_type = ST_POSIX;
 }
 
 TEST(newline_is_escaped_with_quotes)
@@ -749,7 +749,7 @@ TEST(flags_to_str)
 	assert_string_equal("%i", ma_flags_to_str(MF_IGNORE));
 	assert_string_equal("%n", ma_flags_to_str(MF_NO_TERM_MUX));
 
-	assert_string_equal("%N", ma_flags_to_str(MF_KEEP_SESSION));
+	assert_string_equal("%N", ma_flags_to_str(MF_KEEP_IN_FG));
 
 	assert_string_equal("%Pl", ma_flags_to_str(MF_PIPE_FILE_LIST));
 	assert_string_equal("%Pz", ma_flags_to_str(MF_PIPE_FILE_LIST_Z));
diff --git a/tests/misc/expand_status_line_macros.c b/tests/misc/expand_status_line_macros.c
index 71d2041..bf74c5a 100644
--- a/tests/misc/expand_status_line_macros.c
+++ b/tests/misc/expand_status_line_macros.c
@@ -14,6 +14,13 @@
 #include "../../src/utils/str.h"
 #include "../../src/status.h"
 
+/*
+ * Cheatsheet for user-color attributes:
+ *           11111111112
+ * 012345678901234567890
+ * abcdefghijklmnopqrstu
+ */
+
 /* Checks that expanded string isn't equal to format string. */
 #define ASSERT_EXPANDED(format) \
 	do \
@@ -54,7 +61,7 @@
 
 SETUP_ONCE()
 {
-	init_parser(&env_get);
+	vle_parser_init(&env_get);
 	try_enable_utf8_locale();
 }
 
@@ -310,41 +317,48 @@ TEST(highlighting_is_set_correctly)
 {
 	ASSERT_EXPANDED_TO_WITH_HI("[%1*%t%*]",
 	                           "[file]",
-	                           " 1   0");
+	                           " b   a");
 }
 
 TEST(highlighting_is_set_correctly_for_optional)
 {
 	ASSERT_EXPANDED_TO_WITH_HI("%[%1*a",
 	                           "%[a",
-	                           "  1");
+	                           "  b");
 }
 
 TEST(highlighting_has_equals_macro_preserved)
 {
 	ASSERT_EXPANDED_TO_WITH_HI("%9*%t %= %t%7*",
 	                           "file %= file",
-	                           "9    =      ");
+	                           "j    =      ");
 	ASSERT_EXPANDED_TO_WITH_HI("%9*%t%=%t%7*",
 	                           "file%=file",
-	                           "9   =     ");
+	                           "j   =     ");
 	ASSERT_EXPANDED_TO_WITH_HI("%t%9*%=%7*%t",
 	                           "file%=file",
-	                           "    9=7   ");
+	                           "    j=h   ");
 	ASSERT_EXPANDED_TO_WITH_HI("%t%9*%=%7*%t%0*",
 	                           "file%=file",
-	                           "    9=7   ");
+	                           "    j=h   ");
 	ASSERT_EXPANDED_TO_WITH_HI("%=%1*%t%0*",
 	                           "%=file",
-	                           "= 1   ");
+	                           "= b   ");
 	ASSERT_EXPANDED_TO_WITH_HI("%t%1*%=%2*%t%0*",
 	                           "file%=file",
-	                           "    1=2   ");
+	                           "    b=c   ");
+}
+
+TEST(two_digit_user_colors)
+{
+	ASSERT_EXPANDED_TO_WITH_HI("%10*[%11*%t%20*]",
+	                           "[file]",
+	                           "kl   u");
 }
 
 TEST(bad_user_group_remains_in_line)
 {
-	ASSERT_EXPANDED_TO_WITH_HI("%10*", "%10*", "    ");
+	ASSERT_EXPANDED_TO_WITH_HI("%21*", "%21*", "    ");
 }
 
 TEST(empty_optional_drops_attrs)
@@ -358,20 +372,20 @@ TEST(non_empty_optional_preserves_attrs)
 {
 	ASSERT_EXPANDED_TO_WITH_HI("%1*%[%t%2*%t%]%3*",
 	                           "filefile",
-	                           "1   2   ");
+	                           "b   c   ");
 	ASSERT_EXPANDED_TO_WITH_HI("%1*%[%2*%t%3*%t%]%4*",
 	                           "filefile",
-	                           "2   3   ");
+	                           "c   d   ");
 	ASSERT_EXPANDED_TO_WITH_HI("%1*%[%2*%t%3*%]%4*",
 	                           "file",
-	                           "2   ");
+	                           "c   ");
 }
 
 TEST(wide_characters_do_not_break_highlighting, IF(utf8_locale))
 {
 	ASSERT_EXPANDED_TO_WITH_HI("%1*螺丝 %= 螺%2*丝",
 	                           "螺丝 %= 螺丝",
-	                           "1    =    2 ");
+	                           "b    =    c ");
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/tests/misc/filtering.c b/tests/misc/filtering.c
index 897da13..4d4b30d 100644
--- a/tests/misc/filtering.c
+++ b/tests/misc/filtering.c
@@ -265,12 +265,12 @@ TEST(global_local_nature_of_normal_zo)
 	opt_handlers_setup();
 	load_view_options(curr_view);
 
-	init_modes();
-	init_commands();
+	modes_init();
+	cmds_init();
 
 	curr_stats.global_local_settings = 1;
 
-	assert_success(exec_commands("normal zo", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("normal zo", &lwin, CIT_COMMAND));
 	assert_false(lwin.hide_dot_g);
 	assert_false(lwin.hide_dot);
 	assert_false(rwin.hide_dot_g);
@@ -441,11 +441,11 @@ TEST(custom_tree_can_restore_files_after_local_filter_interactive)
 	assert_int_equal(5, lwin.list_rows);
 
 	assert_int_equal(0, local_filter_set(&lwin, "t"));
-	local_filter_accept(&lwin);
+	local_filter_accept(&lwin, /*update_history=*/1);
 	assert_int_equal(2, lwin.list_rows);
 
 	assert_int_equal(0, local_filter_set(&lwin, ""));
-	local_filter_accept(&lwin);
+	local_filter_accept(&lwin, /*update_history=*/1);
 	assert_int_equal(5, lwin.list_rows);
 }
 
diff --git a/tests/misc/find_last_command.c b/tests/misc/find_last_command.c
index f3cc1b4..2d8002a 100644
--- a/tests/misc/find_last_command.c
+++ b/tests/misc/find_last_command.c
@@ -5,7 +5,7 @@
 
 SETUP()
 {
-	init_commands();
+	cmds_init();
 }
 
 TEARDOWN()
@@ -15,22 +15,22 @@ TEARDOWN()
 
 TEST(empty_command)
 {
-	assert_string_equal("", find_last_command(""));
-	assert_string_equal("", find_last_command(":"));
-	assert_string_equal("", find_last_command("::"));
+	assert_string_equal("", cmds_find_last(""));
+	assert_string_equal("", cmds_find_last(":"));
+	assert_string_equal("", cmds_find_last("::"));
 }
 
 TEST(single_command)
 {
-	assert_string_equal("quit", find_last_command("quit"));
+	assert_string_equal("quit", cmds_find_last("quit"));
 }
 
 TEST(commands_with_regex)
 {
-	assert_string_equal("filter a|b", find_last_command("filter a|b"));
-	assert_string_equal("filter /a|b/", find_last_command("filter /a|b/"));
-	assert_string_equal("hi /a|b/", find_last_command("hi /a|b/"));
-	assert_string_equal("next", find_last_command("s/a|b/c/g|next"));
+	assert_string_equal("filter a|b", cmds_find_last("filter a|b"));
+	assert_string_equal("filter /a|b/", cmds_find_last("filter /a|b/"));
+	assert_string_equal("hi /a|b/", cmds_find_last("hi /a|b/"));
+	assert_string_equal("next", cmds_find_last("s/a|b/c/g|next"));
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/tests/misc/flist_custom.c b/tests/misc/flist_custom.c
index aa56a1c..5ce0d8e 100644
--- a/tests/misc/flist_custom.c
+++ b/tests/misc/flist_custom.c
@@ -65,7 +65,7 @@ SETUP()
 
 	snprintf(lwin.curr_dir, sizeof(lwin.curr_dir), "%s/..", test_data);
 
-	init_commands();
+	cmds_init();
 }
 
 TEARDOWN()
@@ -391,13 +391,13 @@ TEST(custom_view_does_not_reset_local_state)
 
 		lwin.columns = columns_create();
 
-		assert_success(exec_commands("set sort=+iname", &lwin, CIT_COMMAND));
+		assert_success(cmds_dispatch("set sort=+iname", &lwin, CIT_COMMAND));
 		assert_int_equal(SK_BY_INAME, lwin.sort[0]);
 
 		local_filter_apply(&lwin, "b");
 
 		/* Neither on entering it. */
-		assert_success(exec_commands("setl sort=-target", &lwin, CIT_COMMAND));
+		assert_success(cmds_dispatch("setl sort=-target", &lwin, CIT_COMMAND));
 		assert_int_equal(-SK_BY_TARGET, lwin.sort[0]);
 		setup_custom_view(&lwin, very);
 		assert_int_equal(very ? SK_NONE : -SK_BY_TARGET, lwin.sort[0]);
@@ -407,7 +407,7 @@ TEST(custom_view_does_not_reset_local_state)
 		cdt.line_hi_group = 1;
 		columns_format_line(lwin.columns, &cdt, MAX_WIDTH);
 
-		assert_success(exec_commands("setl sort=-type", &lwin, CIT_COMMAND));
+		assert_success(cmds_dispatch("setl sort=-type", &lwin, CIT_COMMAND));
 		assert_int_equal(-SK_BY_TYPE, lwin.sort[0]);
 
 		curr_stats.load_stage = 1;
@@ -458,7 +458,8 @@ TEST(files_with_newline_in_names, IF(filenames_can_include_newline))
 
 	create_file("a\nb");
 	assert_non_null(get_cwd(lwin.curr_dir, sizeof(lwin.curr_dir)));
-	assert_success(rn_for_flist(&lwin, "cat list", "title", MF_NONE));
+	assert_success(rn_for_flist(&lwin, "cat list", "title", /*user_sh=*/1,
+				MF_NONE));
 	assert_success(unlink("a\nb"));
 	assert_success(unlink("list"));
 
@@ -487,7 +488,8 @@ TEST(current_directory_can_be_added_via_dot)
 #endif
 	stats_update_shell_type(cfg.shell);
 
-	assert_success(rn_for_flist(&lwin, "echo ../misc", "title", MF_NONE));
+	assert_success(rn_for_flist(&lwin, "echo ../misc", "title", /*user_sh=*/1,
+				MF_NONE));
 
 	stats_update_shell_type("/bin/sh");
 	update_string(&cfg.shell, NULL);
@@ -528,7 +530,7 @@ TEST(built_with_custom_input, IF(have_cat))
 	create_file("a");
 	create_file("b");
 
-	assert_success(rn_for_flist(&lwin, "cat", "title",
+	assert_success(rn_for_flist(&lwin, "cat", "title", /*user_sh=*/1,
 				MF_CUSTOMVIEW_OUTPUT | MF_PIPE_FILE_LIST));
 
 	stats_update_shell_type("/bin/sh");
diff --git a/tests/misc/flist_misc.c b/tests/misc/flist_misc.c
index 5773f23..79f97d2 100644
--- a/tests/misc/flist_misc.c
+++ b/tests/misc/flist_misc.c
@@ -54,7 +54,7 @@ TEST(compare_view_defines_id_grouping)
 {
 	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), TEST_DATA_PATH,
 			"compare/a", cwd);
-	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, 0);
+	compare_one_pane(&lwin, CT_CONTENTS, LT_ALL, CF_NONE);
 
 	assert_int_equal(3, lwin.list_rows);
 
@@ -208,7 +208,7 @@ TEST(find_next_and_prev_mismatches)
 
 	strcpy(lwin.curr_dir, TEST_DATA_PATH "/compare/a");
 	strcpy(rwin.curr_dir, TEST_DATA_PATH "/compare/b");
-	(void)compare_two_panes(CT_CONTENTS, LT_ALL, 1, 0);
+	(void)compare_two_panes(CT_CONTENTS, LT_ALL, CF_GROUP_PATHS | CF_SHOW);
 
 	assert_int_equal(4, lwin.list_rows);
 	assert_int_equal(4, rwin.list_rows);
@@ -461,7 +461,7 @@ TEST(cache_handles_noexec_dirs, IF(regular_unix_user))
 	cached_entries_t cache = {};
 	assert_true(flist_update_cache(&lwin, &cache, SANDBOX_PATH "/dir"));
 	assert_int_equal(0, cache.entries.nentries);
-	flist_free_cache(&lwin, &cache);
+	flist_free_cache(&cache);
 
 	assert_success(chmod(SANDBOX_PATH "/dir", 0777));
 	assert_success(remove(SANDBOX_PATH "/dir/file"));
@@ -534,6 +534,8 @@ TEST(fview_previews_works)
 	lwin.dir_entry[1].type = FT_DIR;
 	strcpy(lwin.curr_dir, "/tests/fake");
 
+	lwin.miller_preview = MP_DIRS;
+
 	lwin.list_pos = 0;
 	assert_false(fview_previews(&lwin, "/tests/fake/file"));
 	lwin.list_pos = 1;
@@ -547,7 +549,16 @@ TEST(fview_previews_works)
 	assert_false(fview_previews(&lwin, "/tests/fake/dir"));
 	assert_false(fview_previews(&lwin, "/unrelated/path"));
 
-	lwin.miller_preview_files = 1;
+	lwin.miller_preview = MP_FILES;
+
+	lwin.list_pos = 0;
+	assert_true(fview_previews(&lwin, "/tests/fake/file"));
+	lwin.list_pos = 1;
+	assert_false(fview_previews(&lwin, "/tests/fake/dir"));
+	assert_false(fview_previews(&lwin, "/unrelated/path"));
+
+	lwin.miller_preview = MP_ALL;
+
 	lwin.list_pos = 0;
 	assert_true(fview_previews(&lwin, "/tests/fake/file"));
 	lwin.list_pos = 1;
@@ -555,5 +566,23 @@ TEST(fview_previews_works)
 	assert_false(fview_previews(&lwin, "/unrelated/path"));
 }
 
+TEST(fentry_points_to_works, IF(not_windows))
+{
+	make_symlink(".", SANDBOX_PATH "/link");
+
+	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), SANDBOX_PATH, "", cwd);
+	load_dir_list(&lwin, 1);
+
+	assert_int_equal(1, lwin.list_rows);
+
+	assert_true(fentry_points_to(&lwin.dir_entry[0], lwin.curr_dir));
+
+	char link_path[PATH_MAX + 1];
+	make_abs_path(link_path, sizeof(link_path), SANDBOX_PATH, "link", cwd);
+	assert_true(fentry_points_to(&lwin.dir_entry[0], link_path));
+
+	remove_file(SANDBOX_PATH "/link");
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/misc/flist_tree_filtering.c b/tests/misc/flist_tree_filtering.c
index 6c0c8c1..8569437 100644
--- a/tests/misc/flist_tree_filtering.c
+++ b/tests/misc/flist_tree_filtering.c
@@ -54,9 +54,6 @@ TEARDOWN()
 	view_teardown(&lwin);
 
 	columns_set_line_print_func(NULL);
-
-	columns_free(lwin.columns);
-	lwin.columns = NULL;
 }
 
 TEST(tree_accounts_for_dot_filter)
@@ -123,7 +120,7 @@ TEST(leafs_are_not_matched_by_local_filtering)
 	assert_int_equal(4, lwin.list_rows);
 
 	assert_int_equal(1, local_filter_set(&lwin, "\\."));
-	local_filter_accept(&lwin);
+	local_filter_accept(&lwin, /*update_history=*/1);
 
 	assert_int_equal(1, lwin.list_rows);
 	validate_tree(&lwin);
@@ -145,7 +142,7 @@ TEST(leafs_are_returned_if_local_filter_is_emptied)
 	assert_success(load_tree(&lwin, SANDBOX_PATH, cwd));
 	assert_int_equal(4, lwin.list_rows);
 	assert_int_equal(0, local_filter_set(&lwin, "."));
-	local_filter_accept(&lwin);
+	local_filter_accept(&lwin, /*update_history=*/1);
 
 	assert_int_equal(2, lwin.list_rows);
 	validate_tree(&lwin);
@@ -156,7 +153,7 @@ TEST(leafs_are_returned_if_local_filter_is_emptied)
 
 	/* ".." should appear after filter is emptied. */
 	assert_int_equal(0, local_filter_set(&lwin, ""));
-	local_filter_accept(&lwin);
+	local_filter_accept(&lwin, /*update_history=*/1);
 	assert_int_equal(4, lwin.list_rows);
 	validate_tree(&lwin);
 
@@ -218,7 +215,7 @@ TEST(nodes_are_reparented_on_filtering)
 	validate_tree(&lwin);
 
 	assert_int_equal(0, local_filter_set(&lwin, "2"));
-	local_filter_accept(&lwin);
+	local_filter_accept(&lwin, /*update_history=*/1);
 	assert_int_equal(2, lwin.list_rows);
 	validate_tree(&lwin);
 }
@@ -230,7 +227,7 @@ TEST(sorting_of_filtered_list_accounts_for_tree)
 	validate_tree(&lwin);
 
 	assert_int_equal(0, local_filter_set(&lwin, "file|dir4"));
-	local_filter_accept(&lwin);
+	local_filter_accept(&lwin, /*update_history=*/1);
 	assert_int_equal(6, lwin.list_rows);
 	validate_tree(&lwin);
 
diff --git a/tests/misc/flist_tree_folding.c b/tests/misc/flist_tree_folding.c
index e670a8d..95e595a 100644
--- a/tests/misc/flist_tree_folding.c
+++ b/tests/misc/flist_tree_folding.c
@@ -14,6 +14,7 @@
 #include "../../src/event_loop.h"
 #include "../../src/filelist.h"
 #include "../../src/filtering.h"
+#include "../../src/running.h"
 #include "../../src/sort.h"
 
 #include "utils.h"
@@ -209,7 +210,7 @@ TEST(folds_of_custom_tree_are_not_lost_on_filtering)
 
 	/* filter */
 	assert_int_equal(0, local_filter_set(&lwin, "[34]"));
-	local_filter_accept(&lwin);
+	local_filter_accept(&lwin, /*update_history=*/1);
 	assert_int_equal(2, lwin.list_rows);
 
 	/* unfold */
@@ -432,6 +433,22 @@ TEST(lazy_unfolding_and_filtering)
 	assert_int_equal(2, lwin.list_rows);
 }
 
+TEST(folding_is_reset_on_leaving_tree)
+{
+	assert_success(load_limited_tree(&lwin, TEST_DATA_PATH "/tree", cwd,
+				/*depth=*/1));
+	assert_true(flist_custom_active(&lwin));
+	assert_int_equal(7, lwin.list_rows);
+
+	lwin.list_pos = 0;
+	assert_string_equal("dir1", lwin.dir_entry[lwin.list_pos].name);
+	rn_open(&lwin, FHE_RUN);
+	assert_false(flist_custom_active(&lwin));
+
+	/* Should do nothing, specifically shouldn't crash. */
+	flist_toggle_fold(&lwin);
+}
+
 static void
 column_line_print(const char buf[], size_t offset, AlignType align,
 		const char full_column[], const format_info_t *info)
diff --git a/tests/misc/format_mount_command.c b/tests/misc/format_mount_command.c
index f9de9f8..e61de94 100644
--- a/tests/misc/format_mount_command.c
+++ b/tests/misc/format_mount_command.c
@@ -1,5 +1,7 @@
 #include <stic.h>
 
+#include <test-utils.h>
+
 #include "../../src/int/fuse.h"
 
 TEST(no_macro_string_unchanged)
@@ -59,7 +61,9 @@ TEST(options_before_macros)
 	int foreground;
 	char buf[256];
 	char format[] = "FUSE_MOUNT2|mount -o opt1=-,opt2 %PARAM %DESTINATION_DIR";
-	char expected[] = "mount -o opt1=-,opt2 param /mnt/point";
+	const char *expected = not_windows()
+	                     ? "mount -o opt1=-,opt2 param /mnt/point"
+	                     : "mount -o opt1=-,opt2 param \"/mnt/point\"";
 
 	format_mount_command("/mnt/point", "/file/path/dir/file", "param", format,
 			sizeof(buf), buf, &foreground);
@@ -87,7 +91,7 @@ TEST(too_long_destination)
 	int foreground;
 	char buf[10];
 	char format[] = "FUSE_MOUNT2|%DESTINATION_DIR";
-	char expected[] = "012345678";
+	const char *expected = (not_windows() ? "012345678" : "\"01234567");
 
 	format_mount_command("0123456789abcdefghijklmnopqrstuvw", "y", "z", format,
 			sizeof(buf), buf, &foreground);
@@ -101,7 +105,7 @@ TEST(too_long_source)
 	int foreground;
 	char buf[10];
 	char format[] = "FUSE_MOUNT2|%SOURCE_FILE";
-	char expected[] = "012345678";
+	const char *expected = (not_windows() ? "012345678" : "\"01234567");
 
 	format_mount_command("a", "0123456789abcdefghijklmnopqrstuvw", "z", format,
 			sizeof(buf), buf, &foreground);
diff --git a/tests/misc/integration.c b/tests/misc/integration.c
index d90aa8a..5605fad 100644
--- a/tests/misc/integration.c
+++ b/tests/misc/integration.c
@@ -215,7 +215,7 @@ TEST(externally_edited_local_filter_is_applied, IF(not_windows))
 	fview_setup();
 	lwin.columns = columns_create();
 
-	init_modes();
+	modes_init();
 	opt_handlers_setup();
 
 	update_string(&cfg.shell, "/bin/sh");
@@ -249,8 +249,6 @@ TEST(externally_edited_local_filter_is_applied, IF(not_windows))
 	update_string(&cfg.shell, NULL);
 	assert_success(unlink(path));
 
-	columns_free(lwin.columns);
-	lwin.columns = NULL;
 	view_teardown(&lwin);
 
 	vle_keys_reset();
@@ -300,5 +298,34 @@ TEST(title_support_is_detected_correctly)
 #endif
 }
 
+TEST(paths_are_escaped_for_the_editor)
+{
+	char *const saved_cwd = save_cwd();
+	assert_success(chdir(SANDBOX_PATH));
+
+	conf_setup();
+	update_string(&cfg.vi_command, "echo success>");
+
+	char content_buf[] = "success";
+	const char *content = content_buf;
+
+	char path_buf[128];
+	char *path = path_buf;
+
+	strcpy(path_buf, "a b c");
+	assert_success(vim_edit_files(/*nfiles=*/1, &path));
+	file_is(path_buf, &content, /*nlines=*/1);
+	remove_file(path_buf);
+
+	strcpy(path_buf, "a & c - + % ; . ,");
+	assert_success(vim_edit_files(/*nfiles=*/1, &path));
+	file_is(path_buf, &content, /*nlines=*/1);
+	remove_file(path_buf);
+
+	conf_teardown();
+
+	restore_cwd(saved_cwd);
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/misc/ipc.c b/tests/misc/ipc.c
index cd8f519..d161f90 100644
--- a/tests/misc/ipc.c
+++ b/tests/misc/ipc.c
@@ -57,7 +57,7 @@ TEST(name_can_be_null, IF(ipc_enabled))
 TEST(name_is_taken_into_account, IF(ipc_enabled))
 {
 	ipc_t *const ipc = ipc_init(NAME, &test_ipc_args, &test_ipc_eval);
-	assert_true(starts_with_lit(ipc_get_name(ipc), NAME));
+	assert_string_starts_with(NAME, ipc_get_name(ipc));
 	ipc_free(ipc);
 }
 
diff --git a/tests/misc/navigation.c b/tests/misc/navigation.c
index 526fbf6..887142b 100644
--- a/tests/misc/navigation.c
+++ b/tests/misc/navigation.c
@@ -67,7 +67,7 @@ TEST(local_filter_is_reset_in_cv_to_follow_mark)
 	assert_true(flist_custom_active(&lwin));
 
 	local_filter_set(&lwin, "b");
-	local_filter_accept(&lwin);
+	local_filter_accept(&lwin, /*update_history=*/1);
 	assert_true(flist_custom_active(&lwin));
 
 	assert_success(marks_goto(&lwin, 'a'));
diff --git a/tests/misc/normal.c b/tests/misc/normal.c
index e7d6e81..83c3d66 100644
--- a/tests/misc/normal.c
+++ b/tests/misc/normal.c
@@ -15,6 +15,7 @@
 #include "../../src/modes/wk.h"
 #include "../../src/ui/ui.h"
 #include "../../src/utils/fs.h"
+#include "../../src/utils/str.h"
 #include "../../src/filelist.h"
 #include "../../src/flist_sel.h"
 #include "../../src/fops_common.h"
@@ -34,7 +35,7 @@ SETUP_ONCE()
 SETUP()
 {
 	view_setup(&lwin);
-	init_modes();
+	modes_init();
 	opt_handlers_setup();
 
 	lwin.sort_g[0] = SK_BY_NAME;
@@ -226,7 +227,7 @@ TEST(gF, IF(not_windows))
 	remove_dir(SANDBOX_PATH "/dir");
 }
 
-TEST(cl, IF(not_windows))
+TEST(cl_on_file, IF(not_windows))
 {
 	conf_setup();
 	undo_setup();
@@ -258,5 +259,59 @@ TEST(cl, IF(not_windows))
 	conf_teardown();
 }
 
+TEST(cl_multiple_files, IF(not_windows))
+{
+	conf_setup();
+	undo_setup();
+	fops_init(&modcline_prompt, NULL);
+
+	create_dir(SANDBOX_PATH "/links");
+	assert_success(make_symlink("froma", SANDBOX_PATH "/links/a"));
+	assert_success(make_symlink("fromb", SANDBOX_PATH "/links/b"));
+
+	create_executable(SANDBOX_PATH "/script");
+	make_file(SANDBOX_PATH "/script",
+			"#!/bin/sh\n"
+			"sed 's/from/to/' < \"$2\" > \"$2_out\"\n"
+			"mv \"$2_out\" \"$2\"\n");
+
+	char vi_cmd[PATH_MAX + 1];
+	make_abs_path(vi_cmd, sizeof(vi_cmd), SANDBOX_PATH, "script", NULL);
+	update_string(&cfg.vi_command, vi_cmd);
+
+	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), SANDBOX_PATH, "links",
+			cwd);
+	populate_dir_list(&lwin, /*reload=*/0);
+
+	/* Selection should open editor. */
+	lwin.dir_entry[0].selected = 1;
+	lwin.dir_entry[1].selected = 1;
+	lwin.selected_files = 2;
+	(void)vle_keys_exec_timed_out(WK_c WK_l);
+
+	char target[PATH_MAX + 1];
+	assert_success(get_link_target(SANDBOX_PATH "/links/a", target,
+				sizeof(target)));
+	assert_string_equal("toa", target);
+	assert_success(get_link_target(SANDBOX_PATH "/links/b", target,
+				sizeof(target)));
+	assert_string_equal("tob", target);
+
+	/* Running cl again should not open editor as there is no selection. */
+	(void)vle_keys_exec_timed_out(WK_c WK_l L"suffix" WK_CR);
+	assert_success(get_link_target(SANDBOX_PATH "/links/a", target,
+				sizeof(target)));
+	assert_string_equal("toasuffix", target);
+
+	remove_file(SANDBOX_PATH "/script");
+	remove_file(SANDBOX_PATH "/links/a");
+	remove_file(SANDBOX_PATH "/links/b");
+	remove_dir(SANDBOX_PATH "/links");
+
+	fops_init(NULL, NULL);
+	undo_teardown();
+	conf_teardown();
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/misc/ops.c b/tests/misc/ops.c
index 7e7547f..86c2d57 100644
--- a/tests/misc/ops.c
+++ b/tests/misc/ops.c
@@ -27,7 +27,7 @@ SETUP()
 {
 	assert_success(chdir(SANDBOX_PATH));
 
-	init_commands();
+	cmds_init();
 	lwin.selected_files = 0;
 	strcpy(lwin.curr_dir, ".");
 	path = NULL;
@@ -37,7 +37,7 @@ SETUP()
 
 TEARDOWN()
 {
-	assert_success(exec_commands("delbmarks!", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("delbmarks!", &lwin, CIT_COMMAND));
 	free(path);
 }
 
@@ -49,7 +49,7 @@ TEST(rename_triggers_bmark_update)
 		fclose(f);
 	}
 
-	assert_success(exec_commands("bmark! old tag", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("bmark! old tag", &lwin, CIT_COMMAND));
 
 	bmarks_list(&bmarks_cb, NULL);
 	assert_string_equal("./old", path);
diff --git a/tests/misc/options.c b/tests/misc/options.c
index 47e1a52..994c9c1 100644
--- a/tests/misc/options.c
+++ b/tests/misc/options.c
@@ -8,8 +8,10 @@
 #include "../../src/cfg/config.h"
 #include "../../src/engine/options.h"
 #include "../../src/engine/text_buffer.h"
+#include "../../src/lua/vlua.h"
 #include "../../src/ui/column_view.h"
 #include "../../src/ui/fileview.h"
+#include "../../src/ui/statusbar.h"
 #include "../../src/ui/ui.h"
 #include "../../src/utils/dynarray.h"
 #include "../../src/utils/gmux.h"
@@ -27,18 +29,15 @@ static int ncols;
 
 SETUP()
 {
-	init_commands();
+	cmds_init();
 
 	view_setup(&lwin);
-
 	lwin.columns = columns_create();
-	lwin.num_width_g = lwin.num_width = 4;
 	lwin.hide_dot_g = lwin.hide_dot = 1;
 	curr_view = &lwin;
 
 	view_setup(&rwin);
 	rwin.columns = columns_create();
-	rwin.num_width_g = rwin.num_width = 4;
 	rwin.hide_dot_g = rwin.hide_dot = 1;
 	other_view = &rwin;
 
@@ -59,11 +58,6 @@ TEARDOWN()
 	view_teardown(&lwin);
 	view_teardown(&rwin);
 
-	columns_free(lwin.columns);
-	lwin.columns = NULL;
-	columns_free(rwin.columns);
-	rwin.columns = NULL;
-
 	columns_teardown();
 }
 
@@ -76,9 +70,9 @@ print_func(const char buf[], size_t offset, AlignType align,
 
 TEST(lsview_block_columns_update_on_sort_change)
 {
-	assert_success(exec_commands("set viewcolumns=", curr_view, CIT_COMMAND));
-	assert_success(exec_commands("set lsview", curr_view, CIT_COMMAND));
-	assert_success(exec_commands("set sort=name", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("set viewcolumns=", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("set lsview", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("set sort=name", curr_view, CIT_COMMAND));
 	/* The check is implicit, an assert will fail if view columns are updated. */
 }
 
@@ -86,15 +80,15 @@ TEST(recovering_from_wrong_viewcolumns_value_works)
 {
 	/* Prepare state required for the test and ensure that it's correct (default
 	 * columns). */
-	assert_success(exec_commands("set viewcolumns={name}", curr_view,
+	assert_success(cmds_dispatch("set viewcolumns={name}", curr_view,
 				CIT_COMMAND));
-	assert_success(exec_commands("set viewcolumns=", curr_view, CIT_COMMAND));
+	assert_success(cmds_dispatch("set viewcolumns=", curr_view, CIT_COMMAND));
 	ncols = 0;
 	columns_format_line(curr_view->columns, NULL, 100);
 	assert_int_equal(2, ncols);
 
 	/* Recovery after wrong string to default state should be done correctly. */
-	assert_failure(exec_commands("set viewcolumns=#4$^", curr_view, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set viewcolumns=#4$^", curr_view, CIT_COMMAND));
 
 	ncols = 0;
 	columns_format_line(curr_view->columns, NULL, 100);
@@ -103,49 +97,37 @@ TEST(recovering_from_wrong_viewcolumns_value_works)
 	assert_string_equal("", vle_opts_get("viewcolumns", OPT_LOCAL));
 }
 
-TEST(set_local_sets_local_value)
-{
-	assert_success(exec_commands("setlocal numberwidth=2", &lwin, CIT_COMMAND));
-	assert_int_equal(4, lwin.num_width_g);
-	assert_int_equal(2, lwin.num_width);
-}
-
-TEST(set_global_sets_global_value)
+TEST(nice_error_on_wrong_viewcolumn_name)
 {
-	assert_success(exec_commands("setglobal numberwidth=2", &lwin, CIT_COMMAND));
-	assert_int_equal(2, lwin.num_width_g);
-	assert_int_equal(4, lwin.num_width);
-}
+	curr_stats.vlua = vlua_init();
 
-TEST(set_sets_local_and_global_values)
-{
-	assert_success(exec_commands("set numberwidth=2", &lwin, CIT_COMMAND));
-	assert_int_equal(2, lwin.num_width_g);
-	assert_int_equal(2, lwin.num_width);
-}
+	ui_sb_msg("");
+	assert_failure(cmds_dispatch("set viewcolumns={bad}", curr_view,
+				CIT_COMMAND));
+	assert_string_equal("Failed to find column: bad\n"
+			"Invalid format of 'viewcolumns' option\n"
+			"Invalid argument for :set command", ui_sb_last());
 
-TEST(fails_to_set_sort_group_with_wrong_regexp)
-{
-	assert_failure(exec_commands("set sortgroups=*", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("set sortgroups=.*,*", &lwin, CIT_COMMAND));
+	vlua_finish(curr_stats.vlua);
+	curr_stats.vlua = NULL;
 }
 
 TEST(dotfiles)
 {
-	assert_success(exec_commands("setlocal dotfiles", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("setlocal dotfiles", &lwin, CIT_COMMAND));
 	assert_true(lwin.hide_dot_g);
 	assert_false(lwin.hide_dot);
 
-	assert_success(exec_commands("setglobal nodotfiles", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("setglobal nodotfiles", &lwin, CIT_COMMAND));
 	assert_true(lwin.hide_dot_g);
 	assert_false(lwin.hide_dot);
 
-	assert_success(exec_commands("set dotfiles", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set dotfiles", &lwin, CIT_COMMAND));
 	assert_false(lwin.hide_dot_g);
 	assert_false(lwin.hide_dot);
 
 	curr_stats.global_local_settings = 1;
-	assert_success(exec_commands("set nodotfiles", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set nodotfiles", &lwin, CIT_COMMAND));
 	assert_true(lwin.hide_dot_g);
 	assert_true(lwin.hide_dot);
 	assert_true(rwin.hide_dot_g);
@@ -164,48 +146,16 @@ TEST(dotfiles)
 
 	rwin.sort_g[0] = SK_BY_NAME;
 
-	assert_success(exec_commands("setlocal dotfiles", &rwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("setlocal dotfiles", &rwin, CIT_COMMAND));
 	reset_local_options(&rwin);
 	vle_tb_clear(vle_err);
 	assert_success(vle_opts_set("dotfiles?", OPT_LOCAL));
 	assert_string_equal("nodotfiles", vle_tb_get_data(vle_err));
 }
 
-TEST(global_local_always_updates_two_views)
-{
-	lwin.ls_view_g = lwin.ls_view = 1;
-	rwin.ls_view_g = rwin.ls_view = 0;
-	lwin.hide_dot_g = lwin.hide_dot = 0;
-	rwin.hide_dot_g = rwin.hide_dot = 1;
-
-	load_view_options(curr_view);
-
-	curr_stats.global_local_settings = 1;
-	assert_success(exec_commands("set nodotfiles lsview", &lwin, CIT_COMMAND));
-	assert_true(lwin.ls_view_g);
-	assert_true(lwin.ls_view);
-	assert_true(rwin.ls_view_g);
-	assert_true(rwin.ls_view);
-	assert_true(lwin.hide_dot_g);
-	assert_true(lwin.hide_dot);
-	assert_true(rwin.hide_dot_g);
-	assert_true(rwin.hide_dot);
-	curr_stats.global_local_settings = 0;
-}
-
-TEST(global_local_updates_regular_options_only_once)
-{
-	cfg.tab_stop = 0;
-
-	curr_stats.global_local_settings = 1;
-	assert_success(exec_commands("set tabstop+=10", &lwin, CIT_COMMAND));
-	assert_int_equal(10, cfg.tab_stop);
-	curr_stats.global_local_settings = 0;
-}
-
 TEST(caseoptions_are_normalized)
 {
-	assert_success(exec_commands("set caseoptions=pPGg", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set caseoptions=pPGg", &lwin, CIT_COMMAND));
 	assert_string_equal("Pg", vle_opts_get("caseoptions", OPT_GLOBAL));
 	assert_int_equal(CO_GOTO_FILE | CO_PATH_COMPL, cfg.case_override);
 	assert_int_equal(CO_GOTO_FILE, cfg.case_ignore);
@@ -218,7 +168,7 @@ TEST(range_in_wordchars_are_inclusive)
 {
 	int i;
 
-	assert_success(exec_commands("set wordchars=a-c,d", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set wordchars=a-c,d", &lwin, CIT_COMMAND));
 
 	for(i = 0; i < 255; ++i)
 	{
@@ -245,29 +195,29 @@ TEST(wrong_ranges_are_handled_properly)
 	}
 
 	/* Inversed range. */
-	assert_failure(exec_commands("set wordchars=c-a", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set wordchars=c-a", &lwin, CIT_COMMAND));
 	assert_success(memcmp(cfg.word_chars, word_chars, 256));
 
 	/* Inversed range with negative beginning. */
-	assert_failure(exec_commands("set wordchars=\xff-10", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set wordchars=\xff-10", &lwin, CIT_COMMAND));
 	assert_success(memcmp(cfg.word_chars, word_chars, 256));
 
 	/* Non single character range. */
-	assert_failure(exec_commands("set wordchars=a-bc", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set wordchars=a-bc", &lwin, CIT_COMMAND));
 	assert_success(memcmp(cfg.word_chars, word_chars, 256));
 
 	/* Half-open ranges. */
-	assert_failure(exec_commands("set wordchars=a-", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set wordchars=a-", &lwin, CIT_COMMAND));
 	assert_success(memcmp(cfg.word_chars, word_chars, 256));
-	assert_failure(exec_commands("set wordchars=-a", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set wordchars=-a", &lwin, CIT_COMMAND));
 	assert_success(memcmp(cfg.word_chars, word_chars, 256));
 
 	/* Bad numerical values. */
-	assert_failure(exec_commands("set wordchars=-1", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set wordchars=-1", &lwin, CIT_COMMAND));
 	assert_success(memcmp(cfg.word_chars, word_chars, 256));
-	assert_failure(exec_commands("set wordchars=666", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set wordchars=666", &lwin, CIT_COMMAND));
 	assert_success(memcmp(cfg.word_chars, word_chars, 256));
-	assert_failure(exec_commands("set wordchars=40-1000", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set wordchars=40-1000", &lwin, CIT_COMMAND));
 	assert_success(memcmp(cfg.word_chars, word_chars, 256));
 }
 
@@ -279,7 +229,7 @@ TEST(sorting_is_set_correctly_on_restart)
 	ui_view_sort_list_ensure_well_formed(&lwin, lwin.sort_g);
 
 	curr_stats.restart_in_progress = 1;
-	assert_success(exec_commands("set sort=+iname", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set sort=+iname", &lwin, CIT_COMMAND));
 	curr_stats.restart_in_progress = 0;
 
 	assert_int_equal(SK_BY_INAME, lwin.sort[0]);
@@ -290,7 +240,7 @@ TEST(fillchars_is_set_on_correct_input)
 {
 	(void)replace_string(&cfg.vborder_filler, "x");
 	(void)replace_string(&cfg.hborder_filler, "y");
-	assert_success(exec_commands("set fillchars=vborder:a,hborder:b", &lwin,
+	assert_success(cmds_dispatch("set fillchars=vborder:a,hborder:b", &lwin,
 				CIT_COMMAND));
 	assert_string_equal("a", cfg.vborder_filler);
 	assert_string_equal("b", cfg.hborder_filler);
@@ -301,7 +251,7 @@ TEST(fillchars_is_set_on_correct_input)
 TEST(fillchars_not_changed_on_wrong_input)
 {
 	(void)replace_string(&cfg.vborder_filler, "x");
-	assert_failure(exec_commands("set fillchars=vorder:a", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set fillchars=vorder:a", &lwin, CIT_COMMAND));
 	assert_string_equal("x", cfg.vborder_filler);
 	update_string(&cfg.vborder_filler, NULL);
 }
@@ -310,8 +260,8 @@ TEST(values_in_fillchars_are_deduplicated)
 {
 	(void)replace_string(&cfg.vborder_filler, "x");
 
-	assert_success(exec_commands("set fillchars=vborder:a", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("set fillchars+=vborder:b", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set fillchars=vborder:a", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set fillchars+=vborder:b", &lwin, CIT_COMMAND));
 	assert_string_equal("b", cfg.vborder_filler);
 	update_string(&cfg.vborder_filler, NULL);
 
@@ -325,18 +275,18 @@ TEST(values_in_fillchars_are_deduplicated)
 
 TEST(fillchars_can_be_reset)
 {
-	assert_success(exec_commands("set fillchars=vborder:v,hborder:h", &lwin,
+	assert_success(cmds_dispatch("set fillchars=vborder:v,hborder:h", &lwin,
 				CIT_COMMAND));
 
-	assert_success(exec_commands("set fillchars=vborder:v", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set fillchars=vborder:v", &lwin, CIT_COMMAND));
 	assert_string_equal("v", cfg.vborder_filler);
 	assert_string_equal("", cfg.hborder_filler);
 
-	assert_success(exec_commands("set fillchars=hborder:h", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set fillchars=hborder:h", &lwin, CIT_COMMAND));
 	assert_string_equal(" ", cfg.vborder_filler);
 	assert_string_equal("h", cfg.hborder_filler);
 
-	assert_success(exec_commands("set fillchars=", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set fillchars=", &lwin, CIT_COMMAND));
 	assert_string_equal(" ", cfg.vborder_filler);
 	assert_string_equal("", cfg.hborder_filler);
 
@@ -346,26 +296,26 @@ TEST(fillchars_can_be_reset)
 
 TEST(sizefmt_is_set_on_correct_input)
 {
-	assert_success(exec_commands("set sizefmt=units:si,precision:1", &lwin,
+	assert_success(cmds_dispatch("set sizefmt=units:si,precision:1", &lwin,
 				CIT_COMMAND));
 
 	cfg.sizefmt.base = -1;
 	cfg.sizefmt.precision = -1;
 
-	assert_success(exec_commands("set sizefmt=units:iec", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set sizefmt=units:iec", &lwin, CIT_COMMAND));
 
 	assert_int_equal(1024, cfg.sizefmt.base);
 	assert_int_equal(0, cfg.sizefmt.precision);
 	assert_int_equal(1, cfg.sizefmt.space);
 
-	assert_success(exec_commands("set sizefmt=units:si,precision:1,space", &lwin,
+	assert_success(cmds_dispatch("set sizefmt=units:si,precision:1,space", &lwin,
 				CIT_COMMAND));
 
 	assert_int_equal(1000, cfg.sizefmt.base);
 	assert_int_equal(1, cfg.sizefmt.precision);
 	assert_int_equal(1, cfg.sizefmt.space);
 
-	assert_success(exec_commands("set sizefmt=units:iec,precision:2,nospace",
+	assert_success(cmds_dispatch("set sizefmt=units:iec,precision:2,nospace",
 				&lwin, CIT_COMMAND));
 
 	assert_int_equal(1024, cfg.sizefmt.base);
@@ -379,12 +329,12 @@ TEST(sizefmt_not_changed_on_wrong_input)
 	cfg.sizefmt.precision = -1;
 	cfg.sizefmt.space = -1;
 
-	assert_failure(exec_commands("set sizefmt=wrong", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("set sizefmt=units:wrong", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("set sizefmt=precision:0", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("set sizefmt=precision:,units:si", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("set sizefmt=space", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("set sizefmt=nospace", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set sizefmt=wrong", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set sizefmt=units:wrong", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set sizefmt=precision:0", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set sizefmt=precision:,units:si", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set sizefmt=space", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set sizefmt=nospace", &lwin, CIT_COMMAND));
 
 	assert_int_equal(-1, cfg.sizefmt.base);
 	assert_int_equal(-1, cfg.sizefmt.precision);
@@ -395,8 +345,8 @@ TEST(values_in_sizefmt_are_deduplicated)
 {
 	(void)replace_string(&cfg.vborder_filler, "x");
 
-	assert_success(exec_commands("set sizefmt=units:si,space", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("set sizefmt+=nospace,units:iec,precision:10", &lwin,
+	assert_success(cmds_dispatch("set sizefmt=units:si,space", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set sizefmt+=nospace,units:iec,precision:10", &lwin,
 				CIT_COMMAND));
 
 	vle_tb_clear(vle_err);
@@ -405,93 +355,52 @@ TEST(values_in_sizefmt_are_deduplicated)
 			vle_tb_get_data(vle_err));
 }
 
-TEST(millerview)
-{
-	assert_success(exec_commands("se millerview", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("se invmillerview", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("setl millerview", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("setl invmillerview", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("setg millerview", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("setg invmillerview", &lwin, CIT_COMMAND));
-}
-
-TEST(milleroptions_handles_wrong_input)
-{
-	assert_failure(exec_commands("se milleroptions=msi:1", &lwin, CIT_COMMAND));
-
-	assert_failure(exec_commands("se milleroptions=lsize:a", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("se milleroptions=csize:a", &lwin, CIT_COMMAND));
-	assert_failure(exec_commands("se milleroptions=rsize:a", &lwin, CIT_COMMAND));
-
-	assert_failure(exec_commands("se milleroptions=csize:0", &lwin, CIT_COMMAND));
-
-	assert_failure(exec_commands("se milleroptions=rpreview:files", &lwin,
-				CIT_COMMAND));
-}
-
-TEST(milleroptions_accepts_correct_input)
-{
-	assert_success(exec_commands("set milleroptions=csize:33,rsize:12,"
-				"rpreview:all", &lwin, CIT_COMMAND));
-
-	vle_tb_clear(vle_err);
-	assert_success(vle_opts_set("milleroptions?", OPT_GLOBAL));
-	assert_string_equal("  milleroptions=lsize:0,csize:33,rsize:12,rpreview:all",
-			vle_tb_get_data(vle_err));
-}
-
-TEST(milleroptions_normalizes_input)
-{
-	assert_success(exec_commands("set milleroptions=lsize:-10,csize:133,"
-				"rpreview:dirs", &lwin, CIT_COMMAND));
-
-	vle_tb_clear(vle_err);
-	assert_success(vle_opts_set("milleroptions?", OPT_GLOBAL));
-	assert_string_equal("  milleroptions=lsize:0,csize:100,rsize:0,rpreview:dirs",
-			vle_tb_get_data(vle_err));
-}
-
 TEST(lsoptions_empty_input)
 {
-	assert_success(exec_commands("set lsoptions=", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set lsoptions=", &lwin, CIT_COMMAND));
 
 	vle_tb_clear(vle_err);
 	assert_success(vle_opts_set("lsoptions?", OPT_GLOBAL));
-	assert_string_equal("  lsoptions=", vle_tb_get_data(vle_err));
+	assert_string_equal("  lsoptions=columncount:0", vle_tb_get_data(vle_err));
 }
 
 TEST(lsoptions_handles_wrong_input)
 {
-	assert_failure(exec_commands("se lsoptions=transposed:yes", &lwin,
+	assert_failure(cmds_dispatch("se lsoptions=transposed:yes", &lwin,
+				CIT_COMMAND));
+	assert_failure(cmds_dispatch("se lsoptions=transpose", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("se lsoptions=columncount:-1", &lwin,
 				CIT_COMMAND));
-	assert_failure(exec_commands("se lsoptions=transpose", &lwin,
+	assert_failure(cmds_dispatch("se lsoptions=columncount:wrong", &lwin,
 				CIT_COMMAND));
 }
 
 TEST(lsoptions_accepts_correct_input)
 {
-	assert_success(exec_commands("set lsview lsoptions=transposed", &lwin,
-				CIT_COMMAND));
+	assert_success(cmds_dispatch("set lsview lsoptions=transposed,columncount:2",
+				&lwin, CIT_COMMAND));
 
 	vle_tb_clear(vle_err);
 	assert_success(vle_opts_set("lsoptions?", OPT_GLOBAL));
-	assert_string_equal("  lsoptions=transposed", vle_tb_get_data(vle_err));
+	assert_string_equal("  lsoptions=columncount:2,transposed",
+			vle_tb_get_data(vle_err));
 }
 
 TEST(lsoptions_normalizes_input)
 {
-	assert_success(exec_commands("set lsoptions=transposed,transposed", &lwin,
+	assert_success(cmds_dispatch("set lsoptions=transposed,transposed", &lwin,
 				CIT_COMMAND));
 
 	vle_tb_clear(vle_err);
 	assert_success(vle_opts_set("lsoptions?", OPT_GLOBAL));
-	assert_string_equal("  lsoptions=transposed", vle_tb_get_data(vle_err));
+	assert_string_equal("  lsoptions=columncount:0,transposed",
+			vle_tb_get_data(vle_err));
 }
 
 TEST(previewprg_updates_state_of_view)
 {
-	assert_success(exec_commands("setg previewprg=gcmd", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("setl previewprg=lcmd", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("setg previewprg=gcmd", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("setl previewprg=lcmd", &lwin, CIT_COMMAND));
 
 	vle_tb_clear(vle_err);
 	assert_success(vle_opts_set("previewprg?", OPT_GLOBAL));
@@ -537,28 +446,28 @@ TEST(tuioptions)
 
 TEST(setting_tabscope_works)
 {
-	assert_success(exec_commands("set tabscope=pane", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set tabscope=pane", &lwin, CIT_COMMAND));
 	assert_true(cfg.pane_tabs);
 
-	assert_success(exec_commands("set tabscope=global", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set tabscope=global", &lwin, CIT_COMMAND));
 	assert_false(cfg.pane_tabs);
 }
 
 TEST(setting_showtabline_works)
 {
-	assert_success(exec_commands("set showtabline=0", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set showtabline=0", &lwin, CIT_COMMAND));
 	assert_int_equal(STL_NEVER, cfg.show_tab_line);
-	assert_success(exec_commands("set showtabline=never", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set showtabline=never", &lwin, CIT_COMMAND));
 	assert_int_equal(STL_NEVER, cfg.show_tab_line);
 
-	assert_success(exec_commands("set showtabline=1", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set showtabline=1", &lwin, CIT_COMMAND));
 	assert_int_equal(STL_MULTIPLE, cfg.show_tab_line);
-	assert_success(exec_commands("set showtabline=multiple", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set showtabline=multiple", &lwin, CIT_COMMAND));
 	assert_int_equal(STL_MULTIPLE, cfg.show_tab_line);
 
-	assert_success(exec_commands("set showtabline=2", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set showtabline=2", &lwin, CIT_COMMAND));
 	assert_int_equal(STL_ALWAYS, cfg.show_tab_line);
-	assert_success(exec_commands("set showtabline=always", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set showtabline=always", &lwin, CIT_COMMAND));
 	assert_int_equal(STL_ALWAYS, cfg.show_tab_line);
 }
 
@@ -569,13 +478,13 @@ TEST(shortmess)
 	cfg.shorten_title_paths = 0;
 	cfg.short_term_mux_titles = 0;
 
-	assert_success(exec_commands("set shortmess=Mp", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set shortmess=Mp", &lwin, CIT_COMMAND));
 	assert_false(cfg.tail_tab_line_paths);
 	assert_true(cfg.short_term_mux_titles);
 	assert_false(cfg.trunc_normal_sb_msgs);
 	assert_true(cfg.shorten_title_paths);
 
-	assert_success(exec_commands("set shortmess=TL", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set shortmess=TL", &lwin, CIT_COMMAND));
 	assert_true(cfg.tail_tab_line_paths);
 	assert_false(cfg.short_term_mux_titles);
 	assert_true(cfg.trunc_normal_sb_msgs);
@@ -586,19 +495,19 @@ TEST(histcursor)
 {
 	cfg.ch_pos_on = 0;
 
-	assert_success(exec_commands("set histcursor=startup", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set histcursor=startup", &lwin, CIT_COMMAND));
 	assert_int_equal(CHPOS_STARTUP, cfg.ch_pos_on);
 
-	assert_success(exec_commands("set histcursor=direnter,dirmark", &lwin,
+	assert_success(cmds_dispatch("set histcursor=direnter,dirmark", &lwin,
 				CIT_COMMAND));
 	assert_int_equal(CHPOS_ENTER | CHPOS_DIRMARK, cfg.ch_pos_on);
 }
 
 TEST(quickview)
 {
-	assert_success(exec_commands("set quickview", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set quickview", &lwin, CIT_COMMAND));
 	assert_true(curr_stats.preview.on);
-	assert_success(exec_commands("set invquickview", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set invquickview", &lwin, CIT_COMMAND));
 	assert_false(curr_stats.preview.on);
 }
 
@@ -611,13 +520,13 @@ TEST(syncregs)
 	char *tmpdir_value = mock_env("TMPDIR", sandbox);
 
 	assert_false(regs_sync_enabled());
-	assert_success(exec_commands("set syncregs=test1", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set syncregs=test1", &lwin, CIT_COMMAND));
 	assert_true(regs_sync_enabled());
-	assert_success(exec_commands("set syncregs=test2", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set syncregs=test2", &lwin, CIT_COMMAND));
 	assert_true(regs_sync_enabled());
-	assert_success(exec_commands("set syncregs=test1", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set syncregs=test1", &lwin, CIT_COMMAND));
 	assert_true(regs_sync_enabled());
-	assert_success(exec_commands("set syncregs=", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set syncregs=", &lwin, CIT_COMMAND));
 	assert_false(regs_sync_enabled());
 
 	/* Make sure nothing is retained from the tests. */
@@ -631,34 +540,34 @@ TEST(syncregs)
 
 TEST(mediaprg, IF(not_windows))
 {
-	assert_success(exec_commands("set mediaprg=prg", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set mediaprg=prg", &lwin, CIT_COMMAND));
 	assert_string_equal("prg", cfg.media_prg);
-	assert_success(exec_commands("set mediaprg=", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set mediaprg=", &lwin, CIT_COMMAND));
 	assert_string_equal("", cfg.media_prg);
 }
 
 TEST(shell)
 {
-	assert_success(exec_commands("set shell=/bin/bash", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set shell=/bin/bash", &lwin, CIT_COMMAND));
 	assert_string_equal("/bin/bash", cfg.shell);
-	assert_success(exec_commands("set sh=/bin/sh", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set sh=/bin/sh", &lwin, CIT_COMMAND));
 	assert_string_equal("/bin/sh", cfg.shell);
 }
 
 TEST(shellcmdflag)
 {
-	assert_success(exec_commands("set shellcmdflag=-ic", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set shellcmdflag=-ic", &lwin, CIT_COMMAND));
 	assert_string_equal("-ic", cfg.shell_cmd_flag);
-	assert_success(exec_commands("set shcf=-c", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set shcf=-c", &lwin, CIT_COMMAND));
 	assert_string_equal("-c", cfg.shell_cmd_flag);
 }
 
 TEST(tablabel)
 {
-	assert_success(exec_commands("set tabprefix={", &lwin, CIT_COMMAND));
-	assert_success(exec_commands("set tablabel=%[(%n)%]%[%[%T{tree}%]{%c}@%]%p:t",
+	assert_success(cmds_dispatch("set tabprefix={", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set tablabel=%[(%n)%]%[%[%T{tree}%]{%c}@%]%p:t",
 				&lwin, CIT_COMMAND));
-	assert_success(exec_commands("set tabsuffix=}", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set tabsuffix=}", &lwin, CIT_COMMAND));
 
 	assert_string_equal("{", cfg.tab_prefix);
 	assert_string_equal("%[(%n)%]%[%[%T{tree}%]{%c}@%]%p:t", cfg.tab_label);
@@ -667,31 +576,31 @@ TEST(tablabel)
 
 TEST(sessionoptions)
 {
-	assert_success(exec_commands("set sessionoptions=dhistory", &lwin,
+	assert_success(cmds_dispatch("set sessionoptions=dhistory", &lwin,
 				CIT_COMMAND));
 	assert_int_equal(VINFO_DHISTORY, cfg.session_options);
 
-	assert_success(exec_commands("set ssop=savedirs,tui", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set ssop=savedirs,tui", &lwin, CIT_COMMAND));
 	assert_int_equal(VINFO_SAVEDIRS | VINFO_TUI, cfg.session_options);
 
-	assert_success(exec_commands("set ssop=", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set ssop=", &lwin, CIT_COMMAND));
 	assert_int_equal(0, cfg.session_options);
 }
 
 TEST(previewoptions)
 {
-	assert_success(exec_commands("set previewoptions=graphicsdelay:12345", &lwin,
+	assert_success(cmds_dispatch("set previewoptions=graphicsdelay:12345", &lwin,
 				CIT_COMMAND));
 	assert_int_equal(12345, cfg.graphics_delay);
 	assert_false(cfg.hard_graphics_clear);
 	assert_int_equal(0, cfg.max_tree_depth);
 	assert_false(cfg.top_tree_stats);
 
-	assert_failure(exec_commands("set previewoptions=graphicsdelay:inf", &lwin,
+	assert_failure(cmds_dispatch("set previewoptions=graphicsdelay:inf", &lwin,
 				CIT_COMMAND));
 	assert_string_equal("Failed to parse \"graphicsdelay\" value: inf",
 			vle_tb_get_data(vle_err));
-	assert_failure(exec_commands("set previewoptions=maxtreedepth:inf", &lwin,
+	assert_failure(cmds_dispatch("set previewoptions=maxtreedepth:inf", &lwin,
 				CIT_COMMAND));
 	assert_string_equal("Failed to parse \"maxtreedepth\" value: inf",
 			vle_tb_get_data(vle_err));
@@ -700,11 +609,11 @@ TEST(previewoptions)
 	assert_int_equal(0, cfg.max_tree_depth);
 	assert_false(cfg.top_tree_stats);
 
-	assert_failure(exec_commands("set previewoptions=graphicsdelay:-12345", &lwin,
+	assert_failure(cmds_dispatch("set previewoptions=graphicsdelay:-12345", &lwin,
 				CIT_COMMAND));
 	assert_string_equal("\"graphicsdelay\" can't be negative, got: -12345",
 			vle_tb_get_data(vle_err));
-	assert_failure(exec_commands("set previewoptions=maxtreedepth:-1", &lwin,
+	assert_failure(cmds_dispatch("set previewoptions=maxtreedepth:-1", &lwin,
 				CIT_COMMAND));
 	assert_string_equal("\"maxtreedepth\" can't be negative, got: -1",
 			vle_tb_get_data(vle_err));
@@ -713,7 +622,7 @@ TEST(previewoptions)
 	assert_int_equal(0, cfg.max_tree_depth);
 	assert_false(cfg.top_tree_stats);
 
-	assert_failure(exec_commands("set previewoptions=graphicsdelay:145,wtf",
+	assert_failure(cmds_dispatch("set previewoptions=graphicsdelay:145,wtf",
 				&lwin, CIT_COMMAND));
 	assert_int_equal(12345, cfg.graphics_delay);
 	assert_false(cfg.hard_graphics_clear);
@@ -722,28 +631,28 @@ TEST(previewoptions)
 	assert_string_equal("Unknown key for 'previewoptions' option: wtf",
 			vle_tb_get_data(vle_err));
 
-	assert_success(exec_commands("set previewoptions=hardgraphicsclear", &lwin,
+	assert_success(cmds_dispatch("set previewoptions=hardgraphicsclear", &lwin,
 				CIT_COMMAND));
 	assert_int_equal(0, cfg.graphics_delay);
 	assert_true(cfg.hard_graphics_clear);
 	assert_int_equal(0, cfg.max_tree_depth);
 	assert_false(cfg.top_tree_stats);
 
-	assert_success(exec_commands("set previewoptions=toptreestats", &lwin,
+	assert_success(cmds_dispatch("set previewoptions=toptreestats", &lwin,
 				CIT_COMMAND));
 	assert_true(cfg.top_tree_stats);
 	assert_int_equal(0, cfg.graphics_delay);
 	assert_int_equal(0, cfg.max_tree_depth);
 	assert_false(cfg.hard_graphics_clear);
 
-	assert_success(exec_commands("set previewoptions=maxtreedepth:10", &lwin,
+	assert_success(cmds_dispatch("set previewoptions=maxtreedepth:10", &lwin,
 				CIT_COMMAND));
 	assert_false(cfg.top_tree_stats);
 	assert_int_equal(0, cfg.graphics_delay);
 	assert_int_equal(10, cfg.max_tree_depth);
 	assert_false(cfg.hard_graphics_clear);
 
-	assert_success(exec_commands("set previewoptions=", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set previewoptions=", &lwin, CIT_COMMAND));
 	assert_int_equal(0, cfg.graphics_delay);
 	assert_false(cfg.hard_graphics_clear);
 	assert_int_equal(0, cfg.max_tree_depth);
@@ -752,23 +661,70 @@ TEST(previewoptions)
 
 TEST(autocd)
 {
-	assert_success(exec_commands("set autocd", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set autocd", &lwin, CIT_COMMAND));
 	assert_true(cfg.auto_cd);
-	assert_success(exec_commands("set noautocd", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set noautocd", &lwin, CIT_COMMAND));
 	assert_false(cfg.auto_cd);
 }
 
 TEST(iooptions)
 {
-	assert_success(exec_commands("set iooptions=fastfilecloning", &lwin,
+	assert_success(cmds_dispatch("set iooptions=fastfilecloning", &lwin,
 				CIT_COMMAND));
 	assert_true(cfg.fast_file_cloning);
 	assert_false(cfg.data_sync);
 
-	assert_success(exec_commands("set iooptions=datasync", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set iooptions=datasync", &lwin, CIT_COMMAND));
 	assert_false(cfg.fast_file_cloning);
 	assert_true(cfg.data_sync);
 }
 
+TEST(mouse)
+{
+	assert_success(cmds_dispatch("set mouse=acmnv", &lwin, CIT_COMMAND));
+	assert_int_equal(M_ALL_MODES | M_NORMAL_MODE | M_VISUAL_MODE |
+			M_CMDLINE_MODE | M_MENU_MODE, cfg.mouse);
+
+	assert_success(cmds_dispatch("set mouse=", &lwin, CIT_COMMAND));
+	assert_int_equal(0, cfg.mouse);
+
+	assert_success(cmds_dispatch("set mouse=cn", &lwin, CIT_COMMAND));
+	assert_int_equal(M_CMDLINE_MODE | M_NORMAL_MODE, cfg.mouse);
+}
+
+TEST(navoptions)
+{
+	vle_tb_clear(vle_err);
+	assert_failure(cmds_dispatch("se navoptions=open:everything", &lwin,
+				CIT_COMMAND));
+	assert_string_equal("Failed to parse \"open\" value: everything",
+			vle_tb_get_data(vle_err));
+
+	vle_tb_clear(vle_err);
+	assert_failure(cmds_dispatch("se navoptions=autoopen:dirs", &lwin,
+				CIT_COMMAND));
+	assert_string_equal("Unknown key for 'navoptions' option: autoopen",
+			vle_tb_get_data(vle_err));
+
+	assert_success(cmds_dispatch("se navoptions=", &lwin, CIT_COMMAND));
+	assert_false(cfg.nav_open_files);
+	assert_success(cmds_dispatch("se navoptions=open:all", &lwin, CIT_COMMAND));
+	assert_true(cfg.nav_open_files);
+	assert_success(cmds_dispatch("se navoptions=open:dirs", &lwin, CIT_COMMAND));
+	assert_false(cfg.nav_open_files);
+
+	/* Failed parsing doesn't update value. */
+	assert_success(cmds_dispatch("se navoptions=open:all", &lwin, CIT_COMMAND));
+	assert_true(cfg.nav_open_files);
+	assert_failure(cmds_dispatch("se navoptions=open:aa", &lwin, CIT_COMMAND));
+	assert_true(cfg.nav_open_files);
+}
+
+TEST(tabline)
+{
+	assert_success(cmds_dispatch("set tabline=asdf", &lwin, CIT_COMMAND));
+	assert_string_equal("asdf", cfg.tab_line);
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/misc/options_classify.c b/tests/misc/options_classify.c
index a042072..d98a1cd 100644
--- a/tests/misc/options_classify.c
+++ b/tests/misc/options_classify.c
@@ -17,7 +17,7 @@
 
 SETUP()
 {
-	init_commands();
+	cmds_init();
 
 	view_setup(&lwin);
 	view_setup(&rwin);
@@ -46,7 +46,7 @@ TEST(classify_parsing_of_types)
 		[FT_REG][DECORATION_SUFFIX][0] = '/',
 	};
 
-	assert_success(exec_commands("set classify=*:reg:/,-:dir:1", &lwin,
+	assert_success(cmds_dispatch("set classify=*:reg:/,-:dir:1", &lwin,
 				CIT_COMMAND));
 
 	assert_int_equal(0,
@@ -61,7 +61,7 @@ TEST(classify_parsing_of_exprs)
 	const char type_decs[FT_COUNT][2][9] = {};
 
 	assert_success(
-			exec_commands(
+			cmds_dispatch(
 				"set classify=*::!{*.c}::/,123::*.c,,*.b::753,b::/.*-.*/i::q,-::*::1",
 				&lwin, CIT_COMMAND));
 
@@ -86,17 +86,17 @@ TEST(classify_suffix_prefix_lengths)
 	char type_decs[FT_COUNT][2][9];
 	memcpy(&type_decs, &cfg.type_decs, sizeof(cfg.type_decs));
 
-	assert_failure(exec_commands("set classify=123456789::{*.c}::/", &lwin,
+	assert_failure(cmds_dispatch("set classify=123456789::{*.c}::/", &lwin,
 				CIT_COMMAND));
 	assert_int_equal(0,
 			memcmp(&cfg.type_decs, &type_decs, sizeof(cfg.type_decs)));
 
-	assert_failure(exec_commands("set classify=::{*.c}::123456789", &lwin,
+	assert_failure(cmds_dispatch("set classify=::{*.c}::123456789", &lwin,
 				CIT_COMMAND));
 	assert_int_equal(0,
 			memcmp(&cfg.type_decs, &type_decs, sizeof(cfg.type_decs)));
 
-	assert_success(exec_commands("set classify=12345678::{*.c}::12345678", &lwin,
+	assert_success(cmds_dispatch("set classify=12345678::{*.c}::12345678", &lwin,
 				CIT_COMMAND));
 }
 
@@ -110,7 +110,7 @@ TEST(classify_pattern_list)
 
 	const char *prefix, *suffix;
 
-	assert_success(exec_commands("set classify=<::{*-data}{*-data}::>", &lwin,
+	assert_success(cmds_dispatch("set classify=<::{*-data}{*-data}::>", &lwin,
 				CIT_COMMAND));
 
 	ui_get_decors(&entry, &prefix, &suffix);
@@ -128,7 +128,7 @@ TEST(classify_can_be_set_to_empty_value)
 
 	const char *prefix, *suffix;
 
-	assert_success(exec_commands("set classify=", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set classify=", &lwin, CIT_COMMAND));
 
 	ui_get_decors(&entry, &prefix, &suffix);
 	assert_string_equal("", prefix);
@@ -146,7 +146,7 @@ TEST(classify_account_assumes_trailing_slashes_for_dirs)
 
 	const char *prefix, *suffix;
 
-	assert_success(exec_commands("set classify=[:dir:],*::*ad::@", &lwin,
+	assert_success(cmds_dispatch("set classify=[:dir:],*::*ad::@", &lwin,
 				CIT_COMMAND));
 
 	ui_get_decors(&entry, &prefix, &suffix);
@@ -156,9 +156,9 @@ TEST(classify_account_assumes_trailing_slashes_for_dirs)
 
 TEST(classify_state_is_not_changed_if_format_is_wong)
 {
-	assert_success(exec_commands("set classify=*::*ad::@", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set classify=*::*ad::@", &lwin, CIT_COMMAND));
 	assert_int_equal(1, cfg.name_dec_count);
-	assert_failure(exec_commands("set classify=*:*ad:@", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set classify=*:*ad:@", &lwin, CIT_COMMAND));
 	assert_int_equal(1, cfg.name_dec_count);
 }
 
@@ -172,7 +172,7 @@ TEST(classify_does_not_stop_on_empty_prefix)
 
 	const char *prefix, *suffix;
 
-	assert_success(exec_commands("set classify=:dir:/,:link:@,:fifo:\\|", &lwin,
+	assert_success(cmds_dispatch("set classify=:dir:/,:link:@,:fifo:\\|", &lwin,
 				CIT_COMMAND));
 
 	entry.type = FT_DIR;
@@ -200,9 +200,9 @@ TEST(changing_classify_invalidates_decors_cache)
 	load_dir_list(&lwin, 1);
 	load_dir_list(&rwin, 1);
 
-	assert_success(exec_commands("set millerview milleroptions=lsize:1,rsize:1",
+	assert_success(cmds_dispatch("set millerview milleroptions=lsize:1,rsize:1",
 				&lwin, CIT_COMMAND));
-	assert_success(exec_commands("set classify=::*.vifm::*\\|,::read::<>", &lwin,
+	assert_success(cmds_dispatch("set classify=::*.vifm::*\\|,::read::<>", &lwin,
 				CIT_COMMAND));
 
 	cfg.columns = 10;
@@ -224,7 +224,7 @@ TEST(changing_classify_invalidates_decors_cache)
 	(void)stats_update_fetch();
 	curr_stats.load_stage = 2;
 	redraw_view(&lwin);
-	assert_success(exec_commands("set classify=", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set classify=", &lwin, CIT_COMMAND));
 	curr_stats.load_stage = 0;
 
 	/* Check that dir_entry_t::name_dec_num of inactive tab are reset. */
@@ -235,8 +235,6 @@ TEST(changing_classify_invalidates_decors_cache)
 	}
 
 	tabs_only(&lwin);
-	columns_free(lwin.columns);
-	lwin.columns = NULL;
 	columns_teardown();
 	cfg.columns = INT_MIN;
 }
diff --git a/tests/misc/options_miller.c b/tests/misc/options_miller.c
new file mode 100644
index 0000000..a532042
--- /dev/null
+++ b/tests/misc/options_miller.c
@@ -0,0 +1,129 @@
+#include <stic.h>
+
+#include <test-utils.h>
+
+#include "../../src/engine/options.h"
+#include "../../src/engine/text_buffer.h"
+#include "../../src/cmd_core.h"
+#include "../../src/ui/column_view.h"
+#include "../../src/ui/tabs.h"
+#include "../../src/ui/ui.h"
+
+static void print_func(const char buf[], size_t offset, AlignType align,
+		const char full_column[], const format_info_t *info);
+
+SETUP()
+{
+	cmds_init();
+
+	view_setup(&lwin);
+	lwin.columns = columns_create();
+	curr_view = &lwin;
+
+	view_setup(&rwin);
+	rwin.columns = columns_create();
+	other_view = &rwin;
+
+	/* Name+size matches default column view setting ("-{name},{}"). */
+	columns_setup_column(SK_BY_NAME);
+	columns_setup_column(SK_BY_SIZE);
+	columns_set_line_print_func(&print_func);
+
+	opt_handlers_setup();
+}
+
+TEARDOWN()
+{
+	opt_handlers_teardown();
+
+	vle_cmds_reset();
+
+	view_teardown(&lwin);
+	view_teardown(&rwin);
+
+	columns_teardown();
+}
+
+TEST(millerview)
+{
+	assert_success(cmds_dispatch("se millerview", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("se invmillerview", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("setl millerview", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("setl invmillerview", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("setg millerview", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("setg invmillerview", &lwin, CIT_COMMAND));
+}
+
+TEST(milleroptions_handles_wrong_input)
+{
+	assert_failure(cmds_dispatch("se milleroptions=msi:1", &lwin, CIT_COMMAND));
+
+	assert_failure(cmds_dispatch("se milleroptions=lsize:a", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("se milleroptions=csize:a", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("se milleroptions=rsize:a", &lwin, CIT_COMMAND));
+
+	assert_failure(cmds_dispatch("se milleroptions=csize:0", &lwin, CIT_COMMAND));
+
+	assert_failure(cmds_dispatch("se milleroptions=rpreview:stuff", &lwin,
+				CIT_COMMAND));
+}
+
+TEST(milleroptions_accepts_correct_input)
+{
+	assert_success(cmds_dispatch("set milleroptions=csize:33,rsize:12,"
+				"rpreview:all", &lwin, CIT_COMMAND));
+
+	vle_tb_clear(vle_err);
+	assert_success(vle_opts_set("milleroptions?", OPT_GLOBAL));
+	assert_string_equal("  milleroptions=lsize:0,csize:33,rsize:12,rpreview:all",
+			vle_tb_get_data(vle_err));
+}
+
+TEST(milleroptions_recovers_after_wrong_input)
+{
+	assert_success(cmds_dispatch("set milleroptions=csize:33,rsize:12,"
+				"rpreview:files", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set milleroptions=rpreview:dirs,rsize:4,"
+				"csize:a", &lwin, CIT_COMMAND));
+
+	vle_tb_clear(vle_err);
+	assert_success(vle_opts_set("milleroptions?", OPT_GLOBAL));
+	assert_string_equal("  milleroptions=lsize:0,csize:33,rsize:12,"
+			"rpreview:files", vle_tb_get_data(vle_err));
+}
+
+TEST(milleroptions_normalizes_input)
+{
+	assert_success(cmds_dispatch("set milleroptions=lsize:-10,csize:133,"
+				"rpreview:dirs", &lwin, CIT_COMMAND));
+
+	vle_tb_clear(vle_err);
+	assert_success(vle_opts_set("milleroptions?", OPT_GLOBAL));
+	assert_string_equal("  milleroptions=lsize:0,csize:100,rsize:0,rpreview:dirs",
+			vle_tb_get_data(vle_err));
+}
+
+TEST(milleroptions_are_cloned_to_new_tabs)
+{
+	init_view_list(&lwin);
+	init_view_list(&rwin);
+
+	assert_success(cmds_dispatch("set milleroptions=lsize:5,rpreview:all", &lwin,
+				CIT_COMMAND));
+
+	tabs_new(NULL, NULL);
+
+	vle_tb_clear(vle_err);
+	assert_success(vle_opts_set("milleroptions?", OPT_GLOBAL));
+	assert_string_equal("  milleroptions=lsize:5,csize:1,rsize:0,rpreview:all",
+			vle_tb_get_data(vle_err));
+}
+
+static void
+print_func(const char buf[], size_t offset, AlignType align,
+		const char full_column[], const format_info_t *info)
+{
+}
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/misc/options_suggestoptions.c b/tests/misc/options_suggestoptions.c
index 5ccbef3..c6200db 100644
--- a/tests/misc/options_suggestoptions.c
+++ b/tests/misc/options_suggestoptions.c
@@ -8,7 +8,7 @@
 
 SETUP()
 {
-	init_commands();
+	cmds_init();
 	curr_view = &lwin;
 	opt_handlers_setup();
 }
@@ -26,7 +26,7 @@ TEST(suggestoptions_all_values)
 	cfg.sug.maxregfiles = 0;
 	cfg.sug.delay = 0;
 
-	assert_success(exec_commands("set suggestoptions=normal,visual,view,otherpane"
+	assert_success(cmds_dispatch("set suggestoptions=normal,visual,view,otherpane"
 				",delay,keys,marks,registers,foldsubkeys", &lwin, CIT_COMMAND));
 
 	assert_int_equal(SF_NORMAL | SF_VISUAL | SF_VIEW | SF_OTHERPANE | SF_DELAY |
@@ -41,7 +41,7 @@ TEST(suggestoptions_wrong_value)
 	cfg.sug.maxregfiles = 0;
 	cfg.sug.delay = 0;
 
-	assert_failure(exec_commands("set suggestoptions=asdf", &lwin, CIT_COMMAND));
+	assert_failure(cmds_dispatch("set suggestoptions=asdf", &lwin, CIT_COMMAND));
 
 	assert_int_equal(0, cfg.sug.flags);
 	assert_int_equal(0, cfg.sug.maxregfiles);
@@ -50,14 +50,14 @@ TEST(suggestoptions_wrong_value)
 
 TEST(suggestoptions_empty_value)
 {
-	assert_success(exec_commands("set suggestoptions=normal", &lwin,
+	assert_success(cmds_dispatch("set suggestoptions=normal", &lwin,
 				CIT_COMMAND));
 
 	cfg.sug.flags = SF_NORMAL;
 	cfg.sug.maxregfiles = 0;
 	cfg.sug.delay = 0;
 
-	assert_success(exec_commands("set suggestoptions=", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("set suggestoptions=", &lwin, CIT_COMMAND));
 
 	assert_int_equal(0, cfg.sug.flags);
 	assert_int_equal(5, cfg.sug.maxregfiles);
@@ -70,15 +70,15 @@ TEST(suggestoptions_registers_number)
 					SF_KEYS | SF_MARKS | SF_REGISTERS | SF_FOLDSUBKEYS;
 	cfg.sug.maxregfiles = 4;
 
-	assert_failure(exec_commands("set suggestoptions=registers:-4", &lwin,
+	assert_failure(cmds_dispatch("set suggestoptions=registers:-4", &lwin,
 				CIT_COMMAND));
 	assert_int_equal(4, cfg.sug.maxregfiles);
 
-	assert_failure(exec_commands("set suggestoptions=registers:0", &lwin,
+	assert_failure(cmds_dispatch("set suggestoptions=registers:0", &lwin,
 				CIT_COMMAND));
 	assert_int_equal(4, cfg.sug.maxregfiles);
 
-	assert_success(exec_commands("set suggestoptions=registers:1", &lwin,
+	assert_success(cmds_dispatch("set suggestoptions=registers:1", &lwin,
 				CIT_COMMAND));
 
 	assert_int_equal(1, cfg.sug.maxregfiles);
@@ -88,15 +88,15 @@ TEST(suggestoptions_delay_number)
 {
 	cfg.sug.delay = 4;
 
-	assert_failure(exec_commands("set suggestoptions=delay:-4", &lwin,
+	assert_failure(cmds_dispatch("set suggestoptions=delay:-4", &lwin,
 				CIT_COMMAND));
 	assert_int_equal(4, cfg.sug.delay);
 
-	assert_success(exec_commands("set suggestoptions=delay:0", &lwin,
+	assert_success(cmds_dispatch("set suggestoptions=delay:0", &lwin,
 				CIT_COMMAND));
 	assert_int_equal(0, cfg.sug.delay);
 
-	assert_success(exec_commands("set suggestoptions=delay:100", &lwin,
+	assert_success(cmds_dispatch("set suggestoptions=delay:100", &lwin,
 				CIT_COMMAND));
 
 	assert_int_equal(100, cfg.sug.delay);
diff --git a/tests/misc/quickview.c b/tests/misc/quickview.c
index c5b378b..a2541b9 100644
--- a/tests/misc/quickview.c
+++ b/tests/misc/quickview.c
@@ -204,7 +204,7 @@ TEST(no_switch_into_view_mode_of_hidden_pane)
 	view_setup(&rwin);
 	make_abs_path(rwin.curr_dir, sizeof(rwin.curr_dir), TEST_DATA_PATH, "", cwd);
 	populate_dir_list(&rwin, 1);
-	init_modes();
+	modes_init();
 
 	curr_stats.preview.on = 1;
 	curr_stats.number_of_windows = 1;
diff --git a/tests/misc/registers.c b/tests/misc/registers.c
index 86553f3..5aca8c8 100644
--- a/tests/misc/registers.c
+++ b/tests/misc/registers.c
@@ -24,6 +24,23 @@ TEARDOWN()
 	regs_reset();
 }
 
+TEST(regs_set_can_clear)
+{
+	const reg_t *reg = regs_find('a');
+
+	regs_append('a', "a");
+	assert_int_equal(1, reg->nfiles);
+	regs_set('a', /*files=*/NULL, /*nfiles=*/0);
+	assert_int_equal(0, reg->nfiles);
+}
+
+TEST(regs_set_handles_invalid_reg_name)
+{
+	char file[] = "file";
+	char *files[] = { file };
+	regs_set('#', files, /*nfiles=*/1);
+}
+
 TEST(suggestion_does_not_print_empty_lines)
 {
 	assert_success(chdir(TEST_DATA_PATH "/existing-files"));
diff --git a/tests/misc/search.c b/tests/misc/search.c
index 7e47d4c..41d199d 100644
--- a/tests/misc/search.c
+++ b/tests/misc/search.c
@@ -12,8 +12,22 @@
 #include "../../src/filelist.h"
 #include "../../src/search.h"
 
+static void set_pos_in_curr_view(int pos);
+
 static char *saved_cwd;
 
+SETUP_ONCE()
+{
+	curr_view = &lwin;
+	other_view = &rwin;
+}
+
+TEARDOWN_ONCE()
+{
+	curr_view = NULL;
+	other_view = NULL;
+}
+
 SETUP()
 {
 	saved_cwd = save_cwd();
@@ -40,12 +54,10 @@ TEARDOWN()
 
 TEST(matches_can_be_highlighted)
 {
-	int found;
-
 	cfg.hl_search = 1;
 
-	find_pattern(&lwin, "dos", 0, 0, &found, 0);
-	assert_true(found);
+	search_pattern(&lwin, "dos", /*stash_selection=*/cfg.hl_search,
+			/*select_matches=*/cfg.hl_search);
 	assert_int_equal(2, lwin.matches);
 	assert_string_equal("dos-eof", lwin.dir_entry[1].name);
 	assert_true(lwin.dir_entry[1].selected);
@@ -57,32 +69,26 @@ TEST(matches_can_be_highlighted)
 
 TEST(nothing_happens_for_empty_pattern)
 {
-	int found;
-
-	find_pattern(&lwin, "", 0, 0, &found, 0);
-	assert_true(found);
+	search_pattern(&lwin, "", /*stash_selection=*/cfg.hl_search,
+			/*select_matches=*/cfg.hl_search);
 	assert_int_equal(0, lwin.matches);
 }
 
 TEST(wrong_pattern_results_in_no_matches)
 {
-	int found;
-
-	find_pattern(&lwin, "asdfasdfasdf", 0, 0, &found, 0);
-	assert_false(found);
+	search_pattern(&lwin, "asdfasdfasdf", /*stash_selection=*/cfg.hl_search,
+			/*select_matches=*/cfg.hl_search);
 	assert_int_equal(0, lwin.matches);
 
-	find_pattern(&lwin, "*", 0, 0, &found, 0);
-	assert_false(found);
+	search_pattern(&lwin, "*", /*stash_selection=*/cfg.hl_search,
+			/*select_matches=*/cfg.hl_search);
 	assert_int_equal(0, lwin.matches);
 }
 
 TEST(search_finds_matching_files_and_numbers_them)
 {
-	int found;
-
-	find_pattern(&lwin, "dos", 0, 0, &found, 0);
-	assert_true(found);
+	search_pattern(&lwin, "dos", /*stash_selection=*/cfg.hl_search,
+			/*select_matches=*/cfg.hl_search);
 	assert_int_equal(2, lwin.matches);
 	assert_string_equal("dos-eof", lwin.dir_entry[1].name);
 	assert_int_equal(1, lwin.dir_entry[1].search_match);
@@ -92,21 +98,19 @@ TEST(search_finds_matching_files_and_numbers_them)
 
 TEST(cursor_can_be_positioned_on_first_match)
 {
-	int found;
-
 	lwin.list_pos = 0;
-	find_pattern(&lwin, "dos", 0, 1, &found, 0);
+	search_pattern(&lwin, "dos", /*stash_selection=*/cfg.hl_search,
+			/*select_matches=*/cfg.hl_search);
 	assert_string_equal("dos-eof", lwin.dir_entry[1].name);
 	lwin.list_pos = 1;
 }
 
 TEST(reset_clears_counter_and_match_numbers)
 {
-	int found;
 	int i;
 
-	find_pattern(&lwin, "dos", 0, 0, &found, 0);
-	assert_true(found);
+	search_pattern(&lwin, "dos", /*stash_selection=*/cfg.hl_search,
+			/*select_matches=*/cfg.hl_search);
 	assert_int_equal(2, lwin.matches);
 
 	reset_search_results(&lwin);
@@ -119,97 +123,111 @@ TEST(reset_clears_counter_and_match_numbers)
 	}
 }
 
-TEST(match_navigation_with_wrapping)
+TEST(match_navigation_in_empty_view)
 {
-	int found;
+	view_teardown(&lwin);
+	view_setup(&lwin);
 
+	assert_int_equal(0, lwin.list_rows);
+	assert_false(goto_search_match(&lwin, /*backward=*/1, /*count=*/1,
+				&set_pos_in_curr_view));
+	assert_false(goto_search_match(&lwin, /*backward=*/0, /*count=*/1,
+				&set_pos_in_curr_view));
+}
+
+TEST(match_navigation_with_wrapping)
+{
 	cfg.wrap_scan = 1;
 
-	find_pattern(&lwin, "dos", 0, 0, &found, 0);
-	assert_true(found);
+	search_pattern(&lwin, "dos", /*stash_selection=*/cfg.hl_search,
+			/*select_matches=*/cfg.hl_search);
 	assert_int_equal(2, lwin.matches);
 
 	lwin.list_pos = 0;
 
 	/* Forward. */
-	goto_search_match(&lwin, 0);
+	goto_search_match(&lwin, /*backward=*/0, /*count=*/1, &set_pos_in_curr_view);
 	assert_int_equal(1, lwin.list_pos);
-	goto_search_match(&lwin, 0);
+	goto_search_match(&lwin, /*backward=*/0, /*count=*/1, &set_pos_in_curr_view);
 	assert_int_equal(2, lwin.list_pos);
-	goto_search_match(&lwin, 0);
+	goto_search_match(&lwin, /*backward=*/0, /*count=*/1, &set_pos_in_curr_view);
 	assert_int_equal(1, lwin.list_pos);
 
 	/* Backward. */
-	goto_search_match(&lwin, 1);
+	goto_search_match(&lwin, /*backward=*/1, /*count=*/1, &set_pos_in_curr_view);
 	assert_int_equal(2, lwin.list_pos);
-	goto_search_match(&lwin, 1);
+	goto_search_match(&lwin, /*backward=*/1, /*count=*/1, &set_pos_in_curr_view);
 	assert_int_equal(1, lwin.list_pos);
-	goto_search_match(&lwin, 1);
+	goto_search_match(&lwin, /*backward=*/1, /*count=*/1, &set_pos_in_curr_view);
 	assert_int_equal(2, lwin.list_pos);
 }
 
 TEST(match_navigation_without_wrapping)
 {
-	int found;
-
 	cfg.wrap_scan = 0;
 
-	find_pattern(&lwin, "dos", 0, 0, &found, 0);
-	assert_true(found);
+	search_pattern(&lwin, "dos", /*stash_selection=*/cfg.hl_search,
+			/*select_matches=*/cfg.hl_search);
 	assert_int_equal(2, lwin.matches);
 
 	lwin.list_pos = 0;
 
 	/* Forward. */
-	goto_search_match(&lwin, 0);
+	goto_search_match(&lwin, /*backward=*/0, /*count=*/1, &set_pos_in_curr_view);
 	assert_int_equal(1, lwin.list_pos);
-	goto_search_match(&lwin, 0);
+	goto_search_match(&lwin, /*backward=*/0, /*count=*/1, &set_pos_in_curr_view);
 	assert_int_equal(2, lwin.list_pos);
-	goto_search_match(&lwin, 0);
+	goto_search_match(&lwin, /*backward=*/0, /*count=*/1, &set_pos_in_curr_view);
 	assert_int_equal(2, lwin.list_pos);
 
 	/* Backward. */
-	goto_search_match(&lwin, 1);
+	goto_search_match(&lwin, /*backward=*/1, /*count=*/1, &set_pos_in_curr_view);
 	assert_int_equal(1, lwin.list_pos);
-	goto_search_match(&lwin, 1);
+	goto_search_match(&lwin, /*backward=*/1, /*count=*/1, &set_pos_in_curr_view);
 	assert_int_equal(1, lwin.list_pos);
 }
 
 TEST(view_patterns_are_synchronized)
 {
-	int found;
-
 	strcpy(rwin.curr_dir, lwin.curr_dir);
 	populate_dir_list(&rwin, 0);
 
-	find_pattern(&rwin, "do", 0, 0, &found, 0);
+	search_pattern(&rwin, "do", /*stash_selection=*/cfg.hl_search,
+			/*select_matches=*/cfg.hl_search);
 	assert_int_equal(2, rwin.matches);
 
-	find_pattern(&lwin, "dos", 0, 0, &found, 0);
+	search_pattern(&lwin, "dos", /*stash_selection=*/cfg.hl_search,
+			/*select_matches=*/cfg.hl_search);
 	assert_int_equal(2, lwin.matches);
 	/* Different patterns cause reset. */
 	assert_int_equal(0, rwin.matches);
 
-	find_pattern(&rwin, "dos", 0, 0, &found, 0);
+	search_pattern(&rwin, "dos", /*stash_selection=*/cfg.hl_search,
+			/*select_matches=*/cfg.hl_search);
 	assert_int_equal(2, rwin.matches);
 	/* Same patterns don't cause reset. */
 	assert_int_equal(2, lwin.matches);
 }
 
-TEST(find_npattern_returns_zero_if_msg_is_not_printed)
+TEST(modnorm_find_returns_zero_if_msg_is_not_printed)
 {
+	int found;
+
 	cfg.hl_search = 1;
-	cfg.inc_search = 0;
+	cfg.wrap_scan = 0;
 
-	assert_int_equal(0, modnorm_find(&lwin, "dos", 0, 1));
+	lwin.list_pos = 2;
+	modnorm_set_search_attrs(/*count=*/1, /*last_search_backward=*/0);
+	assert_int_equal(0, modnorm_find(&lwin, "dos", /*backward=*/0,
+				/*print_msg=*/1, &found));
+	assert_int_equal(2, lwin.list_pos);
+	assert_false(found);
 
 	cfg.hl_search = 0;
 }
 
 TEST(matching_directories)
 {
-	int found;
-
 	cfg.hl_search = 1;
 
 	restore_cwd(saved_cwd);
@@ -219,8 +237,8 @@ TEST(matching_directories)
 	assert_non_null(get_cwd(lwin.curr_dir, sizeof(lwin.curr_dir)));
 	populate_dir_list(&lwin, 0);
 
-	find_pattern(&lwin, "1/", 0, 0, &found, 0);
-	assert_true(found);
+	search_pattern(&lwin, "1/", /*stash_selection=*/cfg.hl_search,
+			/*select_matches=*/cfg.hl_search);
 	assert_string_equal("dir1", lwin.dir_entry[0].name);
 	assert_true(lwin.dir_entry[0].selected);
 	assert_string_equal("dir5", lwin.dir_entry[1].name);
@@ -229,5 +247,11 @@ TEST(matching_directories)
 	cfg.hl_search = 0;
 }
 
+static void
+set_pos_in_curr_view(int pos)
+{
+	curr_view->list_pos = pos;
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/misc/search_functional.c b/tests/misc/search_functional.c
new file mode 100644
index 0000000..f88ad84
--- /dev/null
+++ b/tests/misc/search_functional.c
@@ -0,0 +1,276 @@
+#include <stic.h>
+
+#include <unistd.h> /* chdir() */
+#include <string.h> /* memset() */
+
+#include <test-utils.h>
+
+#include "../../src/cfg/config.h"
+#include "../../src/engine/keys.h"
+#include "../../src/modes/modes.h"
+#include "../../src/modes/wk.h"
+#include "../../src/ui/statusbar.h"
+#include "../../src/utils/fs.h"
+#include "../../src/filelist.h"
+#include "../../src/status.h"
+
+static char *saved_cwd;
+
+SETUP_ONCE()
+{
+	curr_view = &lwin;
+	other_view = &rwin;
+}
+
+TEARDOWN_ONCE()
+{
+	curr_view = NULL;
+	other_view = NULL;
+}
+
+SETUP()
+{
+	saved_cwd = save_cwd();
+
+	view_setup(&lwin);
+	modes_init();
+	opt_handlers_setup();
+
+	assert_success(chdir(TEST_DATA_PATH "/read"));
+	assert_non_null(get_cwd(lwin.curr_dir, sizeof(lwin.curr_dir)));
+
+	lwin.sort[0] = SK_BY_NAME;
+	memset(&lwin.sort[1], SK_NONE, sizeof(lwin.sort) - 1);
+
+	cfg_resize_histories(10);
+
+	populate_dir_list(&lwin, /*reload=*/0);
+
+	assert_int_equal(6, lwin.list_rows);
+	assert_int_equal(0, lwin.list_pos);
+
+	cfg.hl_search = 0;
+	cfg.inc_search = 0;
+}
+
+TEARDOWN()
+{
+	cfg_resize_histories(0);
+
+	opt_handlers_teardown();
+	(void)vle_keys_exec_timed_out(WK_C_c);
+	vle_keys_reset();
+	view_teardown(&lwin);
+
+	restore_cwd(saved_cwd);
+}
+
+TEST(n_works_without_prior_search_in_visual_mode)
+{
+	hists_search_save("asdfasdfasdf");
+
+	ui_sb_msg("");
+
+	(void)vle_keys_exec_timed_out(WK_v WK_n);
+
+	assert_string_equal("Search hit BOTTOM without match for: asdfasdfasdf",
+			ui_sb_last());
+	assert_int_equal(0, lwin.list_pos);
+
+	hists_search_save(".");
+
+	ui_sb_msg("");
+
+	(void)vle_keys_exec_timed_out(WK_n);
+
+	assert_string_equal("(2 of 6) /.", ui_sb_last());
+	assert_int_equal(1, lwin.list_pos);
+	assert_true(lwin.dir_entry[0].selected);
+	assert_true(lwin.dir_entry[1].selected);
+	assert_false(lwin.dir_entry[2].selected);
+}
+
+TEST(hlsearch_is_not_reset_for_invalid_pattern_during_incsearch_in_visual_mode)
+{
+	cfg.hl_search = 1;
+	cfg.inc_search = 1;
+
+	(void)vle_keys_exec_timed_out(WK_v WK_SLASH L"*");
+	assert_true(cfg.hl_search);
+
+	(void)vle_keys_exec_timed_out(WK_C_c);
+}
+
+TEST(message_for_invalid_pattern)
+{
+	ui_sb_msg("");
+
+	(void)vle_keys_exec_timed_out(WK_SLASH L"*" WK_CR);
+
+	assert_string_starts_with("Regexp (*) error: ", ui_sb_last());
+}
+
+TEST(selection_is_dropped_for_hlsearch)
+{
+	cfg.hl_search = 1;
+
+	lwin.dir_entry[0].selected = 1;
+
+	(void)vle_keys_exec_timed_out(WK_SLASH L"dos" WK_CR);
+	assert_false(lwin.dir_entry[0].selected);
+}
+
+TEST(selection_is_not_dropped_for_nohlsearch)
+{
+	cfg.hl_search = 0;
+
+	lwin.dir_entry[0].selected = 1;
+
+	(void)vle_keys_exec_timed_out(WK_SLASH L"dos" WK_CR);
+	assert_true(lwin.dir_entry[0].selected);
+}
+
+TEST(selection_is_not_dropped_in_visual_mode_regardless_of_hlsearch)
+{
+	cfg.hl_search = 1;
+
+	lwin.dir_entry[0].selected = 1;
+
+	(void)vle_keys_exec_timed_out(WK_a WK_v WK_SLASH L"dos" WK_CR);
+	assert_true(lwin.dir_entry[0].selected);
+
+	cfg.hl_search = 0;
+
+	lwin.dir_entry[0].selected = 1;
+
+	(void)vle_keys_exec_timed_out(WK_a WK_v WK_SLASH L"dos" WK_CR);
+	assert_true(lwin.dir_entry[0].selected);
+}
+
+TEST(selection_on_n_with_hlsearch)
+{
+	cfg.hl_search = 1;
+
+	hists_search_save("dos");
+
+	/* Selection IS dropped on the first "n". */
+
+	lwin.dir_entry[0].selected = 1;
+
+	(void)vle_keys_exec_timed_out(WK_n);
+
+	assert_int_equal(1, lwin.list_pos);
+	assert_false(lwin.dir_entry[0].selected);
+	assert_true(lwin.dir_entry[1].selected);
+	assert_true(lwin.dir_entry[2].selected);
+
+	/* Selection IS NOT dropped on the second "n". */
+
+	lwin.dir_entry[0].selected = 1;
+
+	(void)vle_keys_exec_timed_out(WK_n);
+
+	assert_int_equal(2, lwin.list_pos);
+	assert_true(lwin.dir_entry[0].selected);
+	assert_true(lwin.dir_entry[1].selected);
+	assert_true(lwin.dir_entry[2].selected);
+}
+
+TEST(correct_match_number_is_shown_for_search_in_visual_mode)
+{
+	assert_false(lwin.dir_entry[0].selected);
+
+	ui_sb_msg("");
+
+	(void)vle_keys_exec_timed_out(WK_v WK_SLASH L"dos" WK_CR);
+
+	assert_int_equal(2, lwin.matches);
+	assert_int_equal(1, lwin.list_pos);
+	assert_int_equal(0, lwin.dir_entry[0].search_match);
+	assert_int_equal(1, lwin.dir_entry[1].search_match);
+	assert_int_equal(2, lwin.dir_entry[2].search_match);
+	assert_string_starts_with("1 of 2 matching files", ui_sb_last());
+}
+
+TEST(correct_cursor_position_for_incsearch_with_a_count)
+{
+	cfg.inc_search = 1;
+
+	(void)vle_keys_exec_timed_out(L"3" WK_SLASH L".." WK_CR);
+	assert_int_equal(3, lwin.list_pos);
+}
+
+TEST(failed_search_with_a_count_does_not_move_cursor)
+{
+	cfg.wrap_scan = 0;
+
+	(void)vle_keys_exec_timed_out(L"333" WK_SLASH L"." WK_CR);
+	assert_int_equal(0, lwin.list_pos);
+}
+
+TEST(correct_message_and_cursor_position_after_failed_incsearch_with_a_count)
+{
+	cfg.wrap_scan = 0;
+	cfg.inc_search = 1;
+
+	ui_sb_msg("");
+
+	(void)vle_keys_exec_timed_out(L"333" WK_SLASH L"." WK_CR);
+
+	assert_string_equal("Search hit BOTTOM without match for: .", ui_sb_last());
+	assert_int_equal(0, lwin.list_pos);
+}
+
+TEST(message_is_shown_after_incsearch_with_hlsearch_in_visual_mode)
+{
+	cfg.hl_search = 1;
+	cfg.inc_search = 1;
+
+	ui_sb_msg("");
+
+	(void)vle_keys_exec_timed_out(WK_v WK_SLASH L"." WK_CR);
+
+	assert_string_starts_with("2 of 6 matching files", ui_sb_last());
+}
+
+TEST(selection_isnt_dropped_on_empty_input_during_incsearch_with_hls_in_vismode)
+{
+	cfg.hl_search = 1;
+	cfg.inc_search = 1;
+
+	(void)vle_keys_exec_timed_out(WK_v WK_SLASH);
+
+	assert_int_equal(0, lwin.list_pos);
+	assert_true(lwin.dir_entry[0].selected);
+	assert_false(lwin.dir_entry[1].selected);
+
+	(void)vle_keys_exec_timed_out(L"." WK_C_u);
+
+	assert_int_equal(0, lwin.list_pos);
+	assert_true(lwin.dir_entry[0].selected);
+	assert_false(lwin.dir_entry[1].selected);
+
+	(void)vle_keys_exec_timed_out(WK_C_c);
+}
+
+TEST(selection_is_not_leaved_on_cursor_backtracking_during_incsearch_in_vismode)
+{
+	cfg.inc_search = 1;
+
+	(void)vle_keys_exec_timed_out(WK_v WK_SLASH L"dos");
+
+	assert_int_equal(1, lwin.list_pos);
+	assert_true(lwin.dir_entry[0].selected);
+	assert_true(lwin.dir_entry[1].selected);
+
+	(void)vle_keys_exec_timed_out(L"asdfasdfasdf");
+
+	assert_int_equal(0, lwin.list_pos);
+	assert_true(lwin.dir_entry[0].selected);
+	assert_false(lwin.dir_entry[1].selected);
+
+	(void)vle_keys_exec_timed_out(WK_C_c);
+}
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/misc/sort.c b/tests/misc/sort.c
index 83cf510..bad2120 100644
--- a/tests/misc/sort.c
+++ b/tests/misc/sort.c
@@ -463,6 +463,25 @@ TEST(groups_sorting_works)
 	update_string(&lwin.sort_groups, NULL);
 }
 
+TEST(global_groups_sorts_entries_list)
+{
+	update_string(&lwin.sort_groups_g, "([0-9])");
+	(void)regcomp(&lwin.primary_group, "([a-z])", REG_EXTENDED | REG_ICASE);
+
+	lwin.sort_g[0] = SK_BY_GROUPS;
+	lwin.sort_g[1] = SK_BY_NAME;
+	memset(&lwin.sort_g[2], SK_NONE, sizeof(lwin.sort_g) - 2);
+
+	dir_entry_t entry_list[] = { { .name = "a1" }, { .name = "b0" } };
+	entries_t entries = { entry_list, 2 };
+
+	sort_entries(&lwin, entries);
+
+	assert_int_equal(2, entries.nentries);
+	assert_string_equal("b0", entries.entries[0].name);
+	assert_string_equal("a1", entries.entries[1].name);
+}
+
 #ifndef _WIN32
 
 TEST(inode_sorting_works)
diff --git a/tests/misc/sourcing.c b/tests/misc/sourcing.c
index 125ec86..9b6b878 100644
--- a/tests/misc/sourcing.c
+++ b/tests/misc/sourcing.c
@@ -7,7 +7,7 @@
 
 SETUP()
 {
-	init_commands();
+	cmds_init();
 	lwin.selected_files = 0;
 	curr_view = &lwin;
 }
@@ -32,7 +32,7 @@ TEST(trailing_line_continuation_is_ok)
 
 TEST(command_is_executed_only_once)
 {
-	exec_command("let $ENV = 'old'", curr_view, CIT_COMMAND);
+	cmds_dispatch1("let $ENV = 'old'", curr_view, CIT_COMMAND);
 	assert_success(cfg_source_file(TEST_DATA_PATH "/scripts/append-env.vifm"));
 	assert_string_equal("oldvalue", env_get("ENV"));
 }
diff --git a/tests/misc/suite.c b/tests/misc/suite.c
index b5d382e..0aba25c 100644
--- a/tests/misc/suite.c
+++ b/tests/misc/suite.c
@@ -24,7 +24,7 @@ SETUP_ONCE()
 	 * nothing will change the path before we try to save it. */
 	saved_cwd = save_cwd();
 
-	bg_init();
+	assert_success(bg_init());
 
 	tabs_init();
 }
diff --git a/tests/misc/tabs.c b/tests/misc/tabs.c
index b752000..bde2ae2 100644
--- a/tests/misc/tabs.c
+++ b/tests/misc/tabs.c
@@ -569,8 +569,8 @@ TEST(global_local_options_and_tabs)
 TEST(global_local_dotfilter_and_tabs)
 {
 	curr_stats.global_local_settings = 1;
-	init_modes();
-	init_commands();
+	modes_init();
+	cmds_init();
 
 	lwin.hide_dot_g = lwin.hide_dot = 1;
 	rwin.hide_dot_g = rwin.hide_dot = 1;
@@ -580,14 +580,14 @@ TEST(global_local_dotfilter_and_tabs)
 	int i;
 	tab_info_t tab_info;
 
-	assert_success(exec_commands("normal zo", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("normal zo", &lwin, CIT_COMMAND));
 	for(i = 0; tabs_enum_all(i, &tab_info); ++i)
 	{
 		assert_false(tab_info.view->hide_dot_g);
 		assert_false(tab_info.view->hide_dot);
 	}
 
-	assert_success(exec_commands("normal za", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("normal za", &lwin, CIT_COMMAND));
 	for(i = 0; tabs_enum_all(i, &tab_info); ++i)
 	{
 		assert_true(tab_info.view->hide_dot_g);
@@ -602,11 +602,11 @@ TEST(global_local_dotfilter_and_tabs)
 TEST(global_local_manualfilter_and_tabs)
 {
 	curr_stats.global_local_settings = 1;
-	init_modes();
-	init_commands();
+	modes_init();
+	cmds_init();
 
 	tabs_new(NULL, NULL);
-	assert_success(exec_commands("filter /y/", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("filter /y/", &lwin, CIT_COMMAND));
 
 	int i;
 	tab_info_t tab_info;
@@ -645,12 +645,12 @@ TEST(local_options_are_reset_if_path_is_changing)
 TEST(direnter_is_called_for_new_tab)
 {
 	curr_stats.load_stage = -1;
-	init_modes();
-	init_commands();
+	modes_init();
+	cmds_init();
 
 	assert_success(process_set_args("dotfiles", 1, 1));
 
-	assert_success(exec_commands("autocmd DirEnter * setlocal nodotfiles", &lwin,
+	assert_success(cmds_dispatch("autocmd DirEnter * setlocal nodotfiles", &lwin,
 				CIT_COMMAND));
 
 	tabs_new(NULL, SANDBOX_PATH);
@@ -658,7 +658,7 @@ TEST(direnter_is_called_for_new_tab)
 	assert_false(lwin.hide_dot_g);
 	assert_true(lwin.hide_dot);
 
-	assert_success(exec_commands("autocmd!", &lwin, CIT_COMMAND));
+	assert_success(cmds_dispatch("autocmd!", &lwin, CIT_COMMAND));
 
 	vle_keys_reset();
 	vle_cmds_reset();
diff --git a/tests/misc/ui.c b/tests/misc/ui.c
index 886a1ca..5510c78 100644
--- a/tests/misc/ui.c
+++ b/tests/misc/ui.c
@@ -8,6 +8,7 @@
 #include "../../src/cfg/config.h"
 #include "../../src/ui/color_scheme.h"
 #include "../../src/ui/colored_line.h"
+#include "../../src/ui/fileview.h"
 #include "../../src/ui/tabs.h"
 #include "../../src/ui/statusline.h"
 #include "../../src/ui/ui.h"
@@ -288,7 +289,7 @@ TEST(make_tab_expands_current_flag)
 	dispose_tab_title_info(&title_info);
 
 	assert_string_equal("3", title.line);
-	assert_string_equal("2", title.attrs);
+	assert_string_equal("c", title.attrs);
 	cline_dispose(&title);
 
 	title_info = make_tab_title_info(&tab_info, &identity, 1, 0);
@@ -296,7 +297,7 @@ TEST(make_tab_expands_current_flag)
 	dispose_tab_title_info(&title_info);
 
 	assert_string_equal("2", title.line);
-	assert_string_equal("1", title.attrs);
+	assert_string_equal("b", title.attrs);
 	cline_dispose(&title);
 }
 
@@ -373,6 +374,198 @@ TEST(ui_stat_height_works)
 	cfg.display_statusline = 0;
 }
 
+TEST(mouse_map_millerview)
+{
+	/*         left|mid|right
+	 *      --------------------
+	 * 0 row:   012|345|678
+	 * 1 row:      | - |
+	 */
+
+	setup_grid(&lwin, /*column_count=*/1, /*list_rows=*/1, /*init=*/1);
+
+	lwin.ls_view = 0;
+	lwin.miller_view = 1;
+	lwin.miller_ratios[0] = 1;
+	lwin.miller_ratios[1] = 1;
+	lwin.miller_ratios[2] = 1;
+	lwin.top_line = 0;
+	lwin.window_cols = 9;
+	lwin.dir_entry[0].type = FT_DIR;
+
+	assert_int_equal(FVM_LEAVE, fview_map_coordinates(&lwin, 1, 1));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 4, 1));
+	assert_int_equal(FVM_OPEN, fview_map_coordinates(&lwin, 7, 1));
+
+	cfg.extra_padding = 1;
+	assert_int_equal(FVM_LEAVE, fview_map_coordinates(&lwin, 0, 0));
+	assert_int_equal(FVM_LEAVE, fview_map_coordinates(&lwin, 1, 0));
+	assert_int_equal(FVM_LEAVE, fview_map_coordinates(&lwin, 2, 0));
+	assert_int_equal(0, fview_map_coordinates(&lwin, 3, 0));
+	assert_int_equal(0, fview_map_coordinates(&lwin, 4, 0));
+	assert_int_equal(0, fview_map_coordinates(&lwin, 5, 0));
+	assert_int_equal(FVM_OPEN, fview_map_coordinates(&lwin, 6, 0));
+	assert_int_equal(FVM_OPEN, fview_map_coordinates(&lwin, 7, 0));
+	assert_int_equal(FVM_OPEN, fview_map_coordinates(&lwin, 8, 0));
+
+	cfg.extra_padding = 0;
+	assert_int_equal(FVM_LEAVE, fview_map_coordinates(&lwin, 0, 0));
+	assert_int_equal(FVM_LEAVE, fview_map_coordinates(&lwin, 1, 0));
+	assert_int_equal(FVM_LEAVE, fview_map_coordinates(&lwin, 2, 0));
+	assert_int_equal(0, fview_map_coordinates(&lwin, 3, 0));
+	assert_int_equal(0, fview_map_coordinates(&lwin, 4, 0));
+	assert_int_equal(0, fview_map_coordinates(&lwin, 5, 0));
+	assert_int_equal(FVM_OPEN, fview_map_coordinates(&lwin, 6, 0));
+	assert_int_equal(FVM_OPEN, fview_map_coordinates(&lwin, 7, 0));
+	assert_int_equal(FVM_OPEN, fview_map_coordinates(&lwin, 8, 0));
+}
+
+TEST(mouse_map_lsview)
+{
+	lwin.window_rows = 7;
+	setup_grid(&lwin, /*column_count=*/2, /*list_rows=*/11, /*init=*/1);
+
+	lwin.ls_view = 1;
+	lwin.ls_transposed = 0;
+	lwin.miller_view = 0;
+	lwin.top_line = 0;
+	lwin.max_filename_width = 2;
+
+	/*         |0123|4567|89
+	 *      -------------------
+	 * 0 row:  | 00 | 01 | --
+	 * 1 row:  | 02 | 03 | --
+	 * 2 row:  | 04 | 05 | --
+	 * 3 row:  | 06 | 07 | --
+	 * 4 row:  | 08 | 09 | --
+	 * 5 row:  | 10 | -- | --
+	 * 6 row:  | -- | -- | --
+	 */
+
+	cfg.extra_padding = 1;
+	lwin.window_cols = 10;
+	assert_int_equal(0, fview_map_coordinates(&lwin, 0, 0));
+	assert_int_equal(0, fview_map_coordinates(&lwin, 1, 0));
+	assert_int_equal(0, fview_map_coordinates(&lwin, 2, 0));
+	assert_int_equal(0, fview_map_coordinates(&lwin, 3, 0));
+	assert_int_equal(1, fview_map_coordinates(&lwin, 4, 0));
+	assert_int_equal(1, fview_map_coordinates(&lwin, 5, 0));
+	assert_int_equal(1, fview_map_coordinates(&lwin, 6, 0));
+	assert_int_equal(1, fview_map_coordinates(&lwin, 7, 0));
+	assert_int_equal(6, fview_map_coordinates(&lwin, 0, 3));
+	assert_int_equal(6, fview_map_coordinates(&lwin, 1, 3));
+	assert_int_equal(6, fview_map_coordinates(&lwin, 2, 3));
+	assert_int_equal(6, fview_map_coordinates(&lwin, 3, 3));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 8, 0));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 9, 0));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 4, 5));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 5, 5));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 6, 5));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 7, 5));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 2, 6));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 6, 6));
+
+	/*         |012|345|67
+	 *      ----------------
+	 * 0 row:  |00 |01 |--
+	 * 1 row:  |02 |03 |--
+	 * 2 row:  |04 |05 |--
+	 * 3 row:  |06 |07 |--
+	 * 4 row:  |08 |09 |--
+	 * 5 row:  |10 |-- |--
+	 * 6 row:  |-- |-- |--
+	 */
+
+	cfg.extra_padding = 0;
+	lwin.window_cols = 8;
+	assert_int_equal(0, fview_map_coordinates(&lwin, 0, 0));
+	assert_int_equal(0, fview_map_coordinates(&lwin, 1, 0));
+	assert_int_equal(0, fview_map_coordinates(&lwin, 2, 0));
+	assert_int_equal(1, fview_map_coordinates(&lwin, 3, 0));
+	assert_int_equal(1, fview_map_coordinates(&lwin, 4, 0));
+	assert_int_equal(1, fview_map_coordinates(&lwin, 5, 0));
+	assert_int_equal(6, fview_map_coordinates(&lwin, 0, 3));
+	assert_int_equal(6, fview_map_coordinates(&lwin, 1, 3));
+	assert_int_equal(6, fview_map_coordinates(&lwin, 2, 3));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 6, 0));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 7, 0));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 4, 5));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 5, 5));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 6, 5));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 2, 6));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 6, 6));
+}
+
+TEST(mouse_map_tlsview)
+{
+	lwin.window_rows = 6;
+	setup_grid(&lwin, /*column_count=*/2, /*list_rows=*/11, /*init=*/1);
+
+	lwin.ls_view = 1;
+	lwin.ls_transposed = 1;
+	lwin.miller_view = 0;
+	lwin.top_line = 0;
+	lwin.max_filename_width = 2;
+
+	/*         |0123|4567|89
+	 *      -------------------
+	 * 0 row:  | 00 | 06 | --
+	 * 1 row:  | 01 | 07 | --
+	 * 2 row:  | 02 | 08 | --
+	 * 3 row:  | 03 | 09 | --
+	 * 4 row:  | 04 | 10 | --
+	 * 5 row:  | 05 | -- | --
+	 */
+
+	cfg.extra_padding = 1;
+	lwin.window_cols = 10;
+	assert_int_equal(0, fview_map_coordinates(&lwin, 0, 0));
+	assert_int_equal(0, fview_map_coordinates(&lwin, 1, 0));
+	assert_int_equal(0, fview_map_coordinates(&lwin, 2, 0));
+	assert_int_equal(0, fview_map_coordinates(&lwin, 3, 0));
+	assert_int_equal(6, fview_map_coordinates(&lwin, 4, 0));
+	assert_int_equal(6, fview_map_coordinates(&lwin, 5, 0));
+	assert_int_equal(6, fview_map_coordinates(&lwin, 6, 0));
+	assert_int_equal(6, fview_map_coordinates(&lwin, 7, 0));
+	assert_int_equal(3, fview_map_coordinates(&lwin, 0, 3));
+	assert_int_equal(3, fview_map_coordinates(&lwin, 1, 3));
+	assert_int_equal(3, fview_map_coordinates(&lwin, 2, 3));
+	assert_int_equal(3, fview_map_coordinates(&lwin, 3, 3));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 8, 0));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 9, 0));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 4, 5));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 5, 5));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 6, 5));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 7, 5));
+
+	/*         |012|345|67
+	 *      ----------------
+	 * 0 row:  |00 |06 |--
+	 * 1 row:  |01 |07 |--
+	 * 2 row:  |02 |08 |--
+	 * 3 row:  |03 |09 |--
+	 * 4 row:  |04 |10 |--
+	 * 5 row:  |05 |-- |--
+	 */
+
+	cfg.extra_padding = 0;
+	lwin.window_cols = 8;
+	assert_int_equal(0, fview_map_coordinates(&lwin, 0, 0));
+	assert_int_equal(0, fview_map_coordinates(&lwin, 1, 0));
+	assert_int_equal(0, fview_map_coordinates(&lwin, 2, 0));
+	assert_int_equal(6, fview_map_coordinates(&lwin, 3, 0));
+	assert_int_equal(6, fview_map_coordinates(&lwin, 4, 0));
+	assert_int_equal(6, fview_map_coordinates(&lwin, 5, 0));
+	assert_int_equal(3, fview_map_coordinates(&lwin, 0, 3));
+	assert_int_equal(3, fview_map_coordinates(&lwin, 1, 3));
+	assert_int_equal(3, fview_map_coordinates(&lwin, 2, 3));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 6, 0));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 7, 0));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 4, 5));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 5, 5));
+	assert_int_equal(FVM_NONE, fview_map_coordinates(&lwin, 6, 5));
+}
+
 static void
 check_tab_title(const tab_info_t *tab_info, const char text[])
 {
diff --git a/tests/misc/vcache.c b/tests/misc/vcache.c
index d4d0ef6..91ef5a6 100644
--- a/tests/misc/vcache.c
+++ b/tests/misc/vcache.c
@@ -154,7 +154,7 @@ TEST(can_run_viewer_in_current_session)
 	/* Not sure how to test that session hasn't changed without writing a test
 	 * program. */
 	strlist_t lines = vcache_lookup(TEST_DATA_PATH "/read/", "echo text",
-			MF_KEEP_SESSION, VK_TEXTUAL, 1, VC_SYNC, &error);
+			MF_KEEP_IN_FG, VK_TEXTUAL, 1, VC_SYNC, &error);
 	assert_string_equal(NULL, error);
 	assert_int_equal(1, lines.nitems);
 	assert_string_equal("text", lines.items[0]);
diff --git a/tests/misc/view_mode.c b/tests/misc/view_mode.c
index 1a2bc09..4ad2615 100644
--- a/tests/misc/view_mode.c
+++ b/tests/misc/view_mode.c
@@ -40,7 +40,7 @@ SETUP()
 	curr_view = &lwin;
 	other_view = &rwin;
 
-	init_modes();
+	modes_init();
 
 	conf_setup();
 }
@@ -277,7 +277,7 @@ TEST(cmd_v)
 		assert_success(vlua_run_string(curr_stats.vlua, "print(ginfo.action)"));
 		assert_string_equal("edit-one", ui_sb_last());
 		assert_success(vlua_run_string(curr_stats.vlua, "print(ginfo.path)"));
-		assert_true(ends_with(ui_sb_last(), "scripts/append-env.vifm"));
+		assert_string_ends_with("scripts/append-env.vifm", ui_sb_last());
 		assert_success(vlua_run_string(curr_stats.vlua, "print(ginfo.mustwait)"));
 		assert_string_equal("false", ui_sb_last());
 		assert_success(vlua_run_string(curr_stats.vlua, "print(ginfo.line)"));
@@ -329,6 +329,35 @@ TEST(views_with_dirs_can_be_reattached)
 	opt_handlers_teardown();
 }
 
+TEST(previewprg_is_applied)
+{
+	opt_handlers_setup();
+	update_string(&lwin.preview_prg, "echo previewprg_is_applied%%");
+
+	char *error;
+	matchers_t *ms = matchers_alloc("*", 0, 1, "", &error);
+	assert_non_null(ms);
+	ft_set_viewers(ms, "echo viewer%%");
+
+	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), TEST_DATA_PATH, "read",
+			NULL);
+	populate_dir_list(&lwin, 0);
+
+	qv_ensure_is_shown();
+	curr_stats.number_of_windows = 2;
+
+	assert_true(vle_mode_is(NORMAL_MODE));
+	(void)vle_keys_exec_timed_out(WK_C_w WK_w);
+	assert_true(vle_mode_is(VIEW_MODE));
+
+	strlist_t lines = modview_lines(curr_stats.preview.explore);
+	assert_int_equal(1, lines.nitems);
+	assert_string_equal("previewprg_is_applied%", lines.items[0]);
+
+	qv_hide();
+	opt_handlers_teardown();
+}
+
 static int
 start_view_mode(const char pattern[], const char viewers[],
 		const char base_dir[], const char sub_path[])
@@ -343,6 +372,7 @@ start_view_mode(const char pattern[], const char viewers[],
 
 	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), base_dir, sub_path, NULL);
 	populate_dir_list(&lwin, 0);
+
 	(void)vle_keys_exec_timed_out(WK_e);
 
 	return vle_mode_is(VIEW_MODE);
diff --git a/tests/misc/viewport_movement.c b/tests/misc/viewport_movement.c
index e78369f..93396f0 100644
--- a/tests/misc/viewport_movement.c
+++ b/tests/misc/viewport_movement.c
@@ -14,7 +14,7 @@ static view_t *const view = &lwin;
 
 SETUP()
 {
-	init_modes();
+	modes_init();
 	conf_setup();
 
 	view_setup(view);
diff --git a/tests/misc/viewport_moving.c b/tests/misc/viewport_moving.c
index a25ac4b..853cc7f 100644
--- a/tests/misc/viewport_moving.c
+++ b/tests/misc/viewport_moving.c
@@ -12,7 +12,7 @@ SETUP()
 {
 	curr_view = &lwin;
 
-	init_modes();
+	modes_init();
 	opt_handlers_setup();
 }
 
diff --git a/tests/misc/viewport_scrolling.c b/tests/misc/viewport_scrolling.c
index e165e52..3b21550 100644
--- a/tests/misc/viewport_scrolling.c
+++ b/tests/misc/viewport_scrolling.c
@@ -13,7 +13,7 @@ static view_t *const view = &lwin;
 
 SETUP()
 {
-	init_modes();
+	modes_init();
 	conf_setup();
 	columns_setup_column(SK_BY_NAME);
 
@@ -104,6 +104,15 @@ TEST(scrolling_in_ls_visual)
 	assert_int_equal(4, view->top_line);
 	assert_int_equal(13, view->list_pos);
 
+	(void)vle_keys_exec_timed_out(WK_C_y);
+	(void)vle_keys_exec_timed_out(WK_C_y);
+	assert_int_equal(0, view->top_line);
+	assert_int_equal(9, view->list_pos);
+
+	(void)vle_keys_exec_timed_out(WK_C_e);
+	assert_int_equal(2, view->top_line);
+	assert_int_equal(9, view->list_pos);
+
 	modvis_leave(0, 1, 0);
 }
 
@@ -143,8 +152,8 @@ TEST(scrolling_in_tls_normal)
 	assert_int_equal(10, view->list_pos);
 
 	(void)vle_keys_exec_timed_out(WK_C_y);
-	assert_int_equal(8, view->top_line);
-	assert_int_equal(10, view->list_pos);
+	assert_int_equal(0, view->top_line);
+	assert_int_equal(2, view->list_pos);
 
 	(void)vle_keys_exec_timed_out(WK_C_e);
 	assert_int_equal(8, view->top_line);
@@ -177,8 +186,8 @@ TEST(scrolling_in_tls_visual)
 	assert_int_equal(10, view->list_pos);
 
 	(void)vle_keys_exec_timed_out(WK_C_y);
-	assert_int_equal(8, view->top_line);
-	assert_int_equal(10, view->list_pos);
+	assert_int_equal(0, view->top_line);
+	assert_int_equal(2, view->list_pos);
 
 	(void)vle_keys_exec_timed_out(WK_C_e);
 	assert_int_equal(8, view->top_line);
diff --git a/tests/misc/vifminfo.c b/tests/misc/vifminfo.c
index f1ac2ad..4f0ca59 100644
--- a/tests/misc/vifminfo.c
+++ b/tests/misc/vifminfo.c
@@ -85,7 +85,7 @@ TEST(filetypes_are_deduplicated)
 	matchers_t *ms;
 
 	cfg.vifm_info = VINFO_FILETYPES;
-	init_commands();
+	cmds_init();
 
 	/* Add a filetype. */
 	ms = matchers_alloc("*.c", 0, 1, "", &error);
@@ -353,10 +353,6 @@ TEST(view_sorting_round_trip)
 	assert_int_equal(-SK_BY_NAME, rwin.sort_g[5]);
 
 	opt_handlers_teardown();
-	columns_free(lwin.columns);
-	lwin.columns = NULL;
-	columns_free(rwin.columns);
-	rwin.columns = NULL;
 	columns_teardown();
 
 	assert_success(remove(SANDBOX_PATH "/vifminfo.json"));
diff --git a/tests/misc/vifminfo_tabs.c b/tests/misc/vifminfo_tabs.c
index 70acf5a..d549fc4 100644
--- a/tests/misc/vifminfo_tabs.c
+++ b/tests/misc/vifminfo_tabs.c
@@ -199,11 +199,6 @@ TEST(layout_of_pane_tab_is_restored)
 	assert_true(curr_stats.preview.on);
 	tabs_goto(0);
 	assert_false(curr_stats.preview.on);
-
-	columns_free(lwin.columns);
-	lwin.columns = NULL;
-	columns_free(rwin.columns);
-	rwin.columns = NULL;
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/tests/misc/visual.c b/tests/misc/visual.c
index bf2b3ac..d01d325 100644
--- a/tests/misc/visual.c
+++ b/tests/misc/visual.c
@@ -11,12 +11,14 @@
 #include "../../src/compat/fs_limits.h"
 #include "../../src/engine/keys.h"
 #include "../../src/modes/modes.h"
+#include "../../src/modes/visual.h"
 #include "../../src/modes/wk.h"
 #include "../../src/ui/statusbar.h"
 #include "../../src/ui/ui.h"
 #include "../../src/utils/fs.h"
 #include "../../src/utils/str.h"
 #include "../../src/filelist.h"
+#include "../../src/flist_pos.h"
 
 #include "utils.h"
 
@@ -32,7 +34,7 @@ SETUP_ONCE()
 SETUP()
 {
 	view_setup(&lwin);
-	init_modes();
+	modes_init();
 	opt_handlers_setup();
 }
 
@@ -44,7 +46,7 @@ TEARDOWN()
 	opt_handlers_teardown();
 }
 
-TEST(v_after_av_drops_selection)
+TEST(v_after_av_drops_selection_and_doesnt_stash_it)
 {
 	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), TEST_DATA_PATH,
 			"existing-files", cwd);
@@ -75,6 +77,68 @@ TEST(v_after_av_drops_selection)
 	assert_false(lwin.dir_entry[0].selected);
 	assert_false(lwin.dir_entry[1].selected);
 	assert_true(lwin.dir_entry[2].selected);
+
+	/* Leave visual mode rejecting visual selection. */
+	(void)vle_keys_exec_timed_out(WK_C_c);
+
+	assert_false(lwin.dir_entry[0].selected);
+	assert_false(lwin.dir_entry[1].selected);
+	assert_false(lwin.dir_entry[2].selected);
+
+	/* Re-select initially selected (tagged) files. */
+	(void)vle_keys_exec_timed_out(WK_g WK_s);
+
+	/* Stashed selection hasn't been changed by visual mode. */
+	assert_true(lwin.dir_entry[0].selected);
+	assert_true(lwin.dir_entry[1].selected);
+	assert_false(lwin.dir_entry[2].selected);
+}
+
+TEST(modvis_update_after_av_and_cursor_movements)
+{
+	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), TEST_DATA_PATH,
+			"existing-files", cwd);
+	populate_dir_list(&lwin, /*reload=*/0);
+
+	assert_int_equal(3, lwin.list_rows);
+
+	/* Select first file. */
+	(void)vle_keys_exec_timed_out(WK_t);
+
+	/* Enter visual mode with amend at the last file. */
+	(void)vle_keys_exec_timed_out(WK_j WK_j WK_a WK_v);
+
+	modvis_update();
+
+	/* Initially selected (tagged) file is not dropped. */
+	assert_true(lwin.dir_entry[0].selected);
+	assert_false(lwin.dir_entry[1].selected);
+	assert_true(lwin.dir_entry[2].selected);
+
+	/* Interaction with selected files doesn't change visual selection. */
+
+	fpos_set_pos(&lwin, 0);
+	modvis_update();
+
+	assert_true(lwin.dir_entry[0].selected);
+	assert_true(lwin.dir_entry[1].selected);
+	assert_true(lwin.dir_entry[2].selected);
+
+	fpos_set_pos(&lwin, 2);
+	modvis_update();
+
+	assert_true(lwin.dir_entry[0].selected);
+	assert_false(lwin.dir_entry[1].selected);
+	assert_true(lwin.dir_entry[2].selected);
+
+	/* Leave visual mode rejecting visual selection. */
+	(void)vle_keys_exec_timed_out(WK_C_c);
+
+	/* Selection of initially selected (tagged) files hasn't been changed by
+	 * visual mode. */
+	assert_true(lwin.dir_entry[0].selected);
+	assert_false(lwin.dir_entry[1].selected);
+	assert_false(lwin.dir_entry[2].selected);
 }
 
 TEST(cl, IF(not_windows))
diff --git a/tests/misc/visual_restore.c b/tests/misc/visual_restore.c
index 19266b3..abc2ea9 100644
--- a/tests/misc/visual_restore.c
+++ b/tests/misc/visual_restore.c
@@ -21,7 +21,7 @@ SETUP_ONCE()
 
 SETUP()
 {
-	init_modes();
+	modes_init();
 	view_setup(&lwin);
 	opt_handlers_setup();
 
diff --git a/tests/parsing/and_or.c b/tests/parsing/and_or.c
index 7c7e82a..acccaa6 100644
--- a/tests/parsing/and_or.c
+++ b/tests/parsing/and_or.c
@@ -80,28 +80,22 @@ TEST(strings_are_converted_to_integers)
 
 TEST(and_handles_errors_correctly)
 {
-	var_t res_var = var_false();
-
 	char expr[8192];
 	strcpy(expr, "1&&1==");
 	memset(expr + strlen(expr), '3', sizeof(expr) - strlen(expr));
 	expr[sizeof(expr) - 1U] = '\0';
 
-	assert_int_equal(PE_INTERNAL, parse(expr, 0, &res_var));
-	var_free(res_var);
+	ASSERT_FAIL(expr, PE_INTERNAL);
 }
 
 TEST(or_handles_errors_correctly)
 {
-	var_t res_var = var_false();
-
 	char expr[8192];
 	strcpy(expr, "1||1==");
 	memset(expr + strlen(expr), '3', sizeof(expr) - strlen(expr));
 	expr[sizeof(expr) - 1U] = '\0';
 
-	assert_int_equal(PE_INTERNAL, parse(expr, 0, &res_var));
-	var_free(res_var);
+	ASSERT_FAIL(expr, PE_INTERNAL);
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/tests/parsing/asserts.h b/tests/parsing/asserts.h
index a4069c8..c858c28 100644
--- a/tests/parsing/asserts.h
+++ b/tests/parsing/asserts.h
@@ -4,42 +4,65 @@
 #include <stdlib.h> /* free() */
 
 /* This should be a macro to see what test has failed. */
-#define ASSERT_OK(str, result) \
+#define ASSERT_OK(str, expected) \
 	do \
 	{ \
-		char *str_res; \
-		var_t res_var = var_false(); \
-		assert_int_equal(PE_NO_ERROR, parse((str), 0, &res_var)); \
-		str_res = var_to_str(res_var); \
-		assert_string_equal((result), str_res); \
+		parsing_result_t result = vle_parser_eval((str), /*interactive=*/0); \
+		assert_int_equal(PE_NO_ERROR, result.error); \
+		\
+		char *str_res = var_to_str(result.value); \
+		assert_string_equal((expected), str_res); \
 		free(str_res); \
-		var_free(res_var); \
+		\
+		var_free(result.value); \
 	} \
 	while(0)
 
 /* This should be a macro to see what test has failed. */
-#define ASSERT_INT_OK(str, result) \
+#define ASSERT_INT_OK(str, expected) \
 	do \
 	{ \
-		int int_res; \
-		var_t res_var = var_false(); \
-		assert_int_equal(PE_NO_ERROR, parse((str), 0, &res_var)); \
-		int_res = var_to_int(res_var); \
-		assert_int_equal((result), int_res); \
-		var_free(res_var); \
+		parsing_result_t result = vle_parser_eval((str), /*interactive=*/0); \
+		assert_int_equal(PE_NO_ERROR, result.error); \
+		\
+		int int_res = var_to_int(result.value); \
+		assert_int_equal((expected), int_res); \
+		var_free(result.value); \
 	} \
 	while(0)
 
 /* This should be a macro to see what test has failed. */
-#define ASSERT_FAIL(str, error) \
+#define ASSERT_FAIL(str, error_code) \
 	do \
 	{ \
-		var_t res_var = var_false(); \
-		assert_int_equal((error), parse((str), 0, &res_var)); \
-		var_free(res_var); \
+		parsing_result_t result = vle_parser_eval((str), /*interactive=*/0); \
+		assert_int_equal((error_code), result.error); \
+		var_free(result.value); \
 	} \
 	while(0)
 
+/* This should be a macro to see what test has failed. */
+#define ASSERT_FAIL_AT(str, suffix, error_code) \
+	do \
+	{ \
+		parsing_result_t result = vle_parser_eval((str), /*interactive=*/0); \
+		assert_int_equal((error_code), result.error); \
+		var_free(result.value); \
+		\
+		assert_string_equal((suffix), result.last_position); \
+	} \
+	while(0)
+
+/* This should be a macro to see what test has failed. */
+#define ASSERT_FAIL_GET(str, error_code) \
+	({ \
+		parsing_result_t result = vle_parser_eval((str), /*interactive=*/0); \
+		assert_int_equal((error_code), result.error); \
+		var_free(result.value); \
+		\
+		result; \
+	})
+
 #endif /* VIFM_TESTS__PARSING__ASSERTS_H__ */
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/tests/parsing/double_quotes.c b/tests/parsing/double_quotes.c
index 5b3facd..1ec0302 100644
--- a/tests/parsing/double_quotes.c
+++ b/tests/parsing/double_quotes.c
@@ -53,15 +53,12 @@ TEST(dot_ok)
 
 TEST(very_long_string)
 {
-	var_t res_var = var_false();
-
 	char string[8192];
 	string[0] = '\"';
 	memset(string + 1, '0', sizeof(string) - 2U);
 	string[sizeof(string) - 1U] = '\0';
 
-	assert_int_equal(PE_INTERNAL, parse(string, 0, &res_var));
-	var_free(res_var);
+	ASSERT_FAIL(string, PE_INTERNAL);
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/tests/parsing/envvar.c b/tests/parsing/envvar.c
index 8663d5a..1cd5e62 100644
--- a/tests/parsing/envvar.c
+++ b/tests/parsing/envvar.c
@@ -10,7 +10,7 @@ static const char * getenv_value(const char name[]);
 
 SETUP()
 {
-	init_parser(&getenv_value);
+	vle_parser_init(&getenv_value);
 }
 
 static const char *
diff --git a/tests/parsing/functions.c b/tests/parsing/functions.c
index d113efc..790f015 100644
--- a/tests/parsing/functions.c
+++ b/tests/parsing/functions.c
@@ -42,14 +42,12 @@ TEST(function_with_wrong_signature_is_not_added)
 
 TEST(wrong_number_of_arguments_fail)
 {
-	ASSERT_FAIL("a()", PE_INVALID_EXPRESSION);
-	assert_string_equal("a()", get_last_position());
+	ASSERT_FAIL_AT("a()", "a()", PE_INVALID_EXPRESSION);
 }
 
 TEST(wrong_arg_fail)
 {
-	ASSERT_FAIL("a(a)", PE_INVALID_SUBEXPRESSION);
-	assert_string_equal("a)", get_last_position());
+	ASSERT_FAIL_AT("a(a)", "a)", PE_INVALID_SUBEXPRESSION);
 }
 
 TEST(two_args_ok)
diff --git a/tests/parsing/general.c b/tests/parsing/general.c
index 30f1849..49aed22 100644
--- a/tests/parsing/general.c
+++ b/tests/parsing/general.c
@@ -37,20 +37,14 @@ TEST(ends_with_dot_fail)
 
 TEST(fail_position_correct)
 {
-	ASSERT_FAIL("'b' c", PE_INVALID_EXPRESSION);
-	assert_string_equal("'b' c", get_last_position());
-
-	ASSERT_FAIL("a b", PE_INVALID_EXPRESSION);
-	assert_string_equal("a b", get_last_position());
+	ASSERT_FAIL_AT("'b' c", "'b' c", PE_INVALID_EXPRESSION);
+	ASSERT_FAIL_AT("a b", "a b", PE_INVALID_EXPRESSION);
 }
 
 TEST(spaces_and_fail_position_correct)
 {
-	ASSERT_FAIL("  'b' c", PE_INVALID_EXPRESSION);
-	assert_string_equal("'b' c", get_last_position());
-
-	ASSERT_FAIL("  a b", PE_INVALID_EXPRESSION);
-	assert_string_equal("a b", get_last_position());
+	ASSERT_FAIL_AT("  'b' c", "'b' c", PE_INVALID_EXPRESSION);
+	ASSERT_FAIL_AT("  a b", "a b", PE_INVALID_EXPRESSION);
 }
 
 TEST(nothing_but_comment)
@@ -78,16 +72,19 @@ TEST(priority_of_operators)
 
 TEST(state_is_reset_on_each_parsing)
 {
-	ASSERT_FAIL("1 1", PE_INVALID_EXPRESSION);
-	assert_true(is_prev_token_whitespace());
-	ASSERT_FAIL("", PE_INVALID_EXPRESSION);
-	assert_false(is_prev_token_whitespace());
+	parsing_result_t result;
+
+	result = ASSERT_FAIL_GET("1 1", PE_INVALID_EXPRESSION);
+	assert_true(result.ends_with_whitespace);
+
+	result = ASSERT_FAIL_GET("", PE_INVALID_EXPRESSION);
+	assert_false(result.ends_with_whitespace);
 
 	static const function_t function_a = { "a", "adescr", {1,1}, &dummy };
 	assert_success(function_register(&function_a));
 
-	ASSERT_FAIL("a('a'", PE_INVALID_EXPRESSION);
-	assert_int_equal(VTYPE_ERROR, get_parsing_result().type);
+	result = ASSERT_FAIL_GET("a('a'", PE_INVALID_EXPRESSION);
+	assert_int_equal(VTYPE_ERROR, result.value.type);
 
 	function_reset_all();
 }
diff --git a/tests/parsing/numbers.c b/tests/parsing/numbers.c
index e0f3d95..366c33d 100644
--- a/tests/parsing/numbers.c
+++ b/tests/parsing/numbers.c
@@ -80,14 +80,11 @@ TEST(string_is_converted_for_signs)
 
 TEST(extremely_long_number)
 {
-	var_t res_var = var_false();
-
 	char zeroes[8192];
 	memset(zeroes, '0', sizeof(zeroes) - 1U);
 	zeroes[sizeof(zeroes) - 1U] = '\0';
 
-	assert_int_equal(PE_INTERNAL, parse(zeroes, 0, &res_var));
-	var_free(res_var);
+	ASSERT_FAIL(zeroes, PE_INTERNAL);
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/tests/parsing/single_quoted.c b/tests/parsing/single_quoted.c
index 1374706..e04b467 100644
--- a/tests/parsing/single_quoted.c
+++ b/tests/parsing/single_quoted.c
@@ -64,15 +64,12 @@ TEST(dot_ok)
 
 TEST(very_long_string)
 {
-	var_t res_var = var_false();
-
 	char string[8192];
 	string[0] = '\'';
 	memset(string + 1, '0', sizeof(string) - 2U);
 	string[sizeof(string) - 1U] = '\0';
 
-	assert_int_equal(PE_INTERNAL, parse(string, 0, &res_var));
-	var_free(res_var);
+	ASSERT_FAIL(string, PE_INTERNAL);
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/tests/parsing/suite.c b/tests/parsing/suite.c
index 924105d..4f76ed7 100644
--- a/tests/parsing/suite.c
+++ b/tests/parsing/suite.c
@@ -8,7 +8,7 @@ DEFINE_SUITE();
 
 SETUP()
 {
-	init_parser(NULL);
+	vle_parser_init(NULL);
 }
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/tests/regs_shmem_app/regcmd.c b/tests/regs_shmem_app/regcmd.c
index a36202c..36606d9 100644
--- a/tests/regs_shmem_app/regcmd.c
+++ b/tests/regs_shmem_app/regcmd.c
@@ -111,7 +111,7 @@ static void
 regcmd_get(void)
 {
 	int reg_name = regcmd_get_reg_name();
-	reg_t *cnt = regs_find(reg_name);
+	const reg_t *cnt = regs_find(reg_name);
 	if(cnt == NULL)
 	{
 		printf("error,Regsiter name incorrect: %d (%c)\n", reg_name,
diff --git a/tests/test-data/syntax-highlight/syntax.vifm b/tests/test-data/syntax-highlight/syntax.vifm
index c792df0..913b484 100644
--- a/tests/test-data/syntax-highlight/syntax.vifm
+++ b/tests/test-data/syntax-highlight/syntax.vifm
@@ -97,7 +97,8 @@ set trashdir=$HOME/.vifm/trash trash
 set vifminfo=dhistory,save,dirstack,registers,bookmarks "comment
 
 " "$VIFM" should be highlighted as environment variable name
-source $VIFM/vifmrc_local
+" `" comment` should be highlighted as a comment
+source $VIFM/vifmrc_local " comment
 unlet $VIFM
 
 " не подсвечены строки
@@ -427,11 +428,6 @@ unselect! /*/
 unselect ! cmd
 unselect pat
 
-" "echo" should be highlighted as :command
-" "chooseopt" should be highlighted as builtin function
-" "'file'" should be highlighted as a string
-echo chooseopt('file')
-
 " the following should be highlighted as :commands
 dm
 dma
@@ -511,6 +507,13 @@ st
 sto
 stop
 keepsel
+amap
+anoremap
+aunmap
+rege
+reged
+regedi
+regedit
 
 " the words following ":" should be highlighted as :commands
 nnoremap lhs :session
@@ -536,6 +539,17 @@ hi User6
 hi User7
 hi User8
 hi User9
+hi User10
+hi User11
+hi User12
+hi User13
+hi User14
+hi User15
+hi User16
+hi User17
+hi User18
+hi User19
+hi User20
 hi LineNr
 hi OddLine
 
@@ -560,6 +574,9 @@ hi OddLine
 " "tabsuffix" should be highlighted as 'option'
 " "previewoptions" should be highlighted as 'option'
 " "autocd" should be highlighted as 'option'
+" "mouse" should be highlighted as 'option'
+" "navoptions" should be highlighted as 'option'
+" "tabline" and "tal" should be highlighted as 'option'
 set dotfiles nodotfiles invdotfiles dotfiles!
 set caseoptions
 set sizefmt
@@ -580,6 +597,14 @@ set tabprefix
 set tabsuffix
 set previewoptions
 set autocd noautocd invautocd autocd!
+set mouse
+set navoptions
+set tabline tal
+
+" "echo" should be highlighted as :command
+" "chooseopt" should be highlighted as builtin function
+" "'file'" should be highlighted as a string
+echo chooseopt('file')
 
 " "term" should be highlighted as a function()
 echo term('cmd')
@@ -595,6 +620,13 @@ echo fnameescape('path name')
 " string literal
 echo extcached('uses', expand('%c'), expand('stat --format=%%bx%%B %c'))
 
+" "input" should be highlighted as a function()
+" "'prompt: ' should be highlight as a string literal
+echo input('prompt: ')
+
+" "filereadable" should be highlighted as a function()
+echo filereadable('path')
+
 " "let" should be highlighted as a :command
 " "unlet" should be highlighted as a :command
 " "term" should be highlighted as a function()
diff --git a/tests/test-support/test-utils.c b/tests/test-support/test-utils.c
index cac1ee9..6938d05 100644
--- a/tests/test-support/test-utils.c
+++ b/tests/test-support/test-utils.c
@@ -40,6 +40,7 @@
 #include "../../src/filelist.h"
 #include "../../src/filtering.h"
 #include "../../src/opt_handlers.h"
+#include "../../src/plugins.h"
 #include "../../src/status.h"
 #include "../../src/undo.h"
 
@@ -91,6 +92,7 @@ conf_setup(void)
 	update_string(&cfg.vi_x_command, "");
 	update_string(&cfg.ruler_format, "");
 	update_string(&cfg.status_line, "");
+	update_string(&cfg.tab_line, "");
 	update_string(&cfg.grep_prg, "");
 	update_string(&cfg.locate_prg, "");
 	update_string(&cfg.media_prg, "");
@@ -101,6 +103,11 @@ conf_setup(void)
 	update_string(&cfg.tab_suffix, "");
 	update_string(&cfg.delete_prg, "");
 
+	cfg.sizefmt.base = 1;
+	cfg.sizefmt.precision = 0;
+	cfg.sizefmt.ieci_prefixes = 0;
+	cfg.sizefmt.space = 0;
+
 #ifndef _WIN32
 	replace_string(&cfg.shell, "/bin/sh");
 	update_string(&cfg.shell_cmd_flag, "-c");
@@ -126,6 +133,7 @@ conf_teardown(void)
 	update_string(&cfg.vi_x_command, NULL);
 	update_string(&cfg.ruler_format, NULL);
 	update_string(&cfg.status_line, NULL);
+	update_string(&cfg.tab_line, NULL);
 	update_string(&cfg.grep_prg, NULL);
 	update_string(&cfg.locate_prg, NULL);
 	update_string(&cfg.media_prg, NULL);
@@ -139,6 +147,11 @@ conf_teardown(void)
 	update_string(&cfg.shell_cmd_flag, NULL);
 
 	cfg.dot_dirs = 0;
+
+	cfg.sizefmt.base = 0;
+	cfg.sizefmt.precision = 0;
+	cfg.sizefmt.ieci_prefixes = 0;
+	cfg.sizefmt.space = 0;
 }
 
 void
@@ -792,5 +805,17 @@ reset_timestamp(const char path[])
 #endif
 }
 
+void
+load_plugins(struct plugs_t *plugs, const char cfg_dir[])
+{
+	strlist_t plugins_dirs = { };
+	plugins_dirs.nitems = put_into_string_array(&plugins_dirs.items,
+			plugins_dirs.nitems, format_str("%s/plugins", cfg_dir));
+
+	plugs_load(plugs, plugins_dirs);
+
+	free_string_array(plugins_dirs.items, plugins_dirs.nitems);
+}
+
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
 /* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/test-support/test-utils.h b/tests/test-support/test-utils.h
index 0ca54f5..a4af7d6 100644
--- a/tests/test-support/test-utils.h
+++ b/tests/test-support/test-utils.h
@@ -171,6 +171,12 @@ void stub_colmgr(void);
 /* Changes time attributes of a file to something "long time ago". */
 void reset_timestamp(const char path[]);
 
+struct plugs_t;
+
+/* Appends "/plugins" to the passed in directory and loads plugins from
+ * there. */
+void load_plugins(struct plugs_t *plugs, const char cfg_dir[]);
+
 #endif /* VIFM_TESTS__TEST_SUPPORT__TEST_UTILS_H__ */
 
 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
diff --git a/tests/utils/build_path.c b/tests/utils/build_path.c
new file mode 100644
index 0000000..86b8b91
--- /dev/null
+++ b/tests/utils/build_path.c
@@ -0,0 +1,75 @@
+#include <stic.h>
+
+#include "../../src/compat/fs_limits.h"
+#include "../../src/utils/path.h"
+
+TEST(non_empty_parts)
+{
+	char path[PATH_MAX + 1];
+
+	build_path(path, sizeof(path), "a", "b");
+	assert_string_equal("a/b", path);
+
+	build_path(path, sizeof(path), "a/", "b");
+	assert_string_equal("a/b", path);
+
+	build_path(path, sizeof(path), "a", "/b");
+	assert_string_equal("a/b", path);
+
+	build_path(path, sizeof(path), "a/", "/b");
+	assert_string_equal("a/b", path);
+}
+
+TEST(empty_head)
+{
+	char path[PATH_MAX + 1];
+
+	build_path(path, sizeof(path), "", "b");
+	assert_string_equal("/b", path);
+
+	build_path(path, sizeof(path), "/", "b");
+	assert_string_equal("/b", path);
+
+	build_path(path, sizeof(path), "", "/b");
+	assert_string_equal("/b", path);
+
+	build_path(path, sizeof(path), "/", "/b");
+	assert_string_equal("/b", path);
+}
+
+TEST(empty_tail)
+{
+	char path[PATH_MAX + 1];
+
+	build_path(path, sizeof(path), "a", "");
+	assert_string_equal("a", path);
+
+	build_path(path, sizeof(path), "a/", "");
+	assert_string_equal("a/", path);
+
+	build_path(path, sizeof(path), "a", "");
+	assert_string_equal("a", path);
+
+	build_path(path, sizeof(path), "a/", "");
+	assert_string_equal("a/", path);
+}
+
+TEST(both_parts_empty)
+{
+	char path[PATH_MAX + 1];
+
+	build_path(path, sizeof(path), "", "");
+	assert_string_equal("", path);
+
+	build_path(path, sizeof(path), "", "");
+	assert_string_equal("", path);
+
+	build_path(path, sizeof(path), "", "");
+	assert_string_equal("", path);
+
+	build_path(path, sizeof(path), "", "");
+	assert_string_equal("", path);
+}
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/utils/friendly_size.c b/tests/utils/friendly_size.c
index 716d274..b50bdfe 100644
--- a/tests/utils/friendly_size.c
+++ b/tests/utils/friendly_size.c
@@ -1,7 +1,6 @@
 #include <stic.h>
 
 #include "../../src/cfg/config.h"
-#include "../../src/utils/str.h"
 #include "../../src/utils/utils.h"
 
 SETUP()
@@ -122,8 +121,8 @@ TEST(huge_precision)
 	 * exact match. */
 	cfg.sizefmt.precision = 25;
 	friendly_size_notation(231093, sizeof(buf), buf);
-	assert_true(starts_with_lit(buf, "225.676757812"));
-	assert_true(ends_with(buf, " K"));
+	assert_string_starts_with("225.676757812", buf);
+	assert_string_ends_with(" K", buf);
 }
 
 TEST(nospace)
diff --git a/tests/utils/fswatch.c b/tests/utils/fswatch.c
index c42b17d..c98156c 100644
--- a/tests/utils/fswatch.c
+++ b/tests/utils/fswatch.c
@@ -91,9 +91,11 @@ TEST(target_replacement_is_detected, IF(using_inotify))
 
 	assert_int_equal(FSWS_UNCHANGED, fswatch_poll(watch));
 
+	/* Creating a new directory before removing watched one and later doing a
+	 * rename to guarantee that inode number won't match. */
+	assert_success(os_mkdir(SANDBOX_PATH "/newinode", 0700));
 	assert_success(remove(SANDBOX_PATH "/testdir"));
-	assert_success(os_mkdir(SANDBOX_PATH "/eatinode", 0700));
-	assert_success(os_mkdir(SANDBOX_PATH "/testdir", 0700));
+	assert_success(os_rename(SANDBOX_PATH "/newinode", SANDBOX_PATH "/testdir"));
 
 	assert_int_equal(FSWS_REPLACED, fswatch_poll(watch));
 	assert_int_equal(FSWS_UNCHANGED, fswatch_poll(watch));
@@ -101,7 +103,6 @@ TEST(target_replacement_is_detected, IF(using_inotify))
 	fswatch_free(watch);
 
 	assert_success(remove(SANDBOX_PATH "/testdir"));
-	assert_success(remove(SANDBOX_PATH "/eatinode"));
 }
 
 TEST(target_mount_is_detected, IF(using_inotify))
diff --git a/tests/utils/make_tmp_file.c b/tests/utils/make_tmp_file.c
new file mode 100644
index 0000000..2207c99
--- /dev/null
+++ b/tests/utils/make_tmp_file.c
@@ -0,0 +1,74 @@
+#include <stic.h>
+
+#include <errno.h> /* EINVAL errno */
+#include <stdio.h> /* FILE fputs() fclose() */
+
+#include <test-utils.h>
+
+#include "../../src/compat/fs_limits.h"
+#include "../../src/compat/os.h"
+#include "../../src/utils/fs.h"
+#include "../../src/utils/str.h"
+
+TEST(make_tmp_file_no_auto_delete)
+{
+	char path[] = SANDBOX_PATH "/tmp-XXXXXX";
+	FILE *fp = make_tmp_file(path, 0600, /*auto_delete=*/0);
+	assert_non_null(fp);
+	assert_false(ends_with(path, "XXXXXX"));
+
+	fputs("line1\n", fp);
+	fputs("line2\n", fp);
+	fclose(fp);
+
+	const char *lines[] = { "line1", "line2" };
+	file_is(path, lines, 2);
+	remove_file(path);
+}
+
+TEST(make_tmp_file_auto_delete)
+{
+	char path[] = SANDBOX_PATH "/tmp-XXXXXX";
+	FILE *fp = make_tmp_file(path, 0000, /*auto_delete=*/1);
+	assert_non_null(fp);
+	fclose(fp);
+	no_remove_file(path);
+}
+
+TEST(make_tmp_file_bad_pattern)
+{
+	char path[] = SANDBOX_PATH "/tmp-XXXXX";
+	assert_null(make_tmp_file(path, 0000, /*auto_delete=*/0));
+	assert_int_equal(EINVAL, errno);
+}
+
+TEST(make_file_in_tmp_slash_prefix)
+{
+	char buf[PATH_MAX + 1];
+	assert_null(make_file_in_tmp("b/a/d", 0000, /*auto_delete=*/0, buf,
+				sizeof(buf)));
+	assert_int_equal(EINVAL, errno);
+}
+
+TEST(make_file_in_tmp_tiny_buffer)
+{
+	char buf[5];
+	assert_null(make_file_in_tmp("prefix", 0000, /*auto_delete=*/0, buf,
+				sizeof(buf)));
+	assert_int_equal(ERANGE, errno);
+}
+
+TEST(bad_location, IF(regular_unix_user))
+{
+	create_dir(SANDBOX_PATH "/ro-dir");
+	assert_success(os_chmod(SANDBOX_PATH "/ro-dir", 0000));
+
+	char path[] = SANDBOX_PATH "/ro-dir/tmp-XXXXXX";
+	assert_null(make_tmp_file(path, 0000, /*auto_delete=*/0));
+	assert_int_equal(EACCES, errno);
+
+	remove_dir(SANDBOX_PATH "/ro-dir");
+}
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 filetype=c : */
diff --git a/tests/utils/read_file_lines.c b/tests/utils/read_file_lines.c
index 88558d6..cff7c87 100644
--- a/tests/utils/read_file_lines.c
+++ b/tests/utils/read_file_lines.c
@@ -7,8 +7,18 @@
 
 TEST(non_existing_file)
 {
-	int nlines;
+	int nlines = -1;
 	char **lines = read_file_of_lines(TEST_DATA_PATH "/read/wrong-path", &nlines);
+	assert_null(lines);
+	assert_int_equal(-1, nlines);
+	free(lines);
+}
+
+TEST(empty_file)
+{
+	int nlines = -1;
+	char **lines =
+		read_file_of_lines(TEST_DATA_PATH "/existing-files/a", &nlines);
 	assert_non_null(lines);
 	assert_int_equal(0, nlines);
 	free(lines);
diff --git a/tests/utils/tilde.c b/tests/utils/tilde.c
new file mode 100644
index 0000000..127f020
--- /dev/null
+++ b/tests/utils/tilde.c
@@ -0,0 +1,60 @@
+#include <stic.h>
+
+#include <stdio.h> /* snprintf() */
+#include <stdlib.h> /* free() */
+#include <string.h> /* strcpy() */
+
+#include "../../src/cfg/config.h"
+#include "../../src/compat/fs_limits.h"
+#include "../../src/utils/path.h"
+
+TEST(no_tilde)
+{
+	char *expanded = expand_tilde("/no/tilde");
+	assert_string_equal("/no/tilde", expanded);
+	free(expanded);
+}
+
+TEST(just_the_tilde)
+{
+	strcpy(cfg.home_dir, "/homedir/");
+
+	char *expanded = expand_tilde("~");
+	assert_string_equal("/homedir/", expanded);
+	free(expanded);
+}
+
+TEST(tilde_and_slash)
+{
+	strcpy(cfg.home_dir, "/homedir/");
+
+	char *expanded = expand_tilde("~/");
+	assert_string_equal("/homedir/", expanded);
+	free(expanded);
+}
+
+TEST(tilde_and_path)
+{
+	strcpy(cfg.home_dir, "/homedir/");
+
+	char *expanded = expand_tilde("~/path");
+	assert_string_equal("/homedir/path", expanded);
+	free(expanded);
+}
+
+TEST(invalid_user_name)
+{
+	char *expanded = expand_tilde("~6r|o_o-t0#a!!!");
+	assert_string_equal("~6r|o_o-t0#a!!!", expanded);
+	free(expanded);
+}
+
+TEST(huge_user_name_after_tilde)
+{
+	char path[PATH_MAX*2 + 1];
+	snprintf(path, sizeof(path), "~%*s/", (int)sizeof(path) - 7, "name");
+
+	char *expanded = expand_tilde(path);
+	assert_string_equal(path, expanded);
+	free(expanded);
+}
diff --git a/tests/utils/utf8.c b/tests/utils/utf8.c
index 1dea3f0..4544688 100644
--- a/tests/utils/utf8.c
+++ b/tests/utils/utf8.c
@@ -67,6 +67,29 @@ TEST(length_is_less_or_equal_to_string_length, IF(utf8_locale))
 	}
 }
 
+TEST(utf8_nstrsw_works, IF(utf8_locale))
+{
+	const char str[] = "师从abаб";
+
+	/* Full chars. */
+	assert_int_equal(0, utf8_nstrsw(str, strlen("")));
+	assert_int_equal(2, utf8_nstrsw(str, strlen("师")));
+	assert_int_equal(4, utf8_nstrsw(str, strlen("师从")));
+	assert_int_equal(5, utf8_nstrsw(str, strlen("师从a")));
+	assert_int_equal(6, utf8_nstrsw(str, strlen("师从ab")));
+	assert_int_equal(7, utf8_nstrsw(str, strlen("师从abа")));
+	assert_int_equal(8, utf8_nstrsw(str, strlen("师从abаб")));
+	assert_int_equal(8, utf8_nstrsw(str, strlen("师从abаб   ")));
+
+	/* Cut char in half. */
+	int max = strlen("师");
+	int i;
+	for(i = 1; i < max; ++i)
+	{
+		assert_int_equal(2, utf8_nstrsw(str, i));
+	}
+}
+
 #ifdef _WIN32
 
 TEST(utf16_roundtrip, IF(utf8_locale))

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/debug/.build-id/b5/d9f0e27028e6d597d7707b86b69db88f56fe1a.debug
-rw-r--r--  root/root   /usr/share/doc/vifm/ChangeLog.LuaAPI.gz
-rw-r--r--  root/root   /usr/share/icons/hicolor/128x128/apps/vifm.png
-rw-r--r--  root/root   /usr/share/icons/hicolor/scalable/apps/vifm.svg

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/lib/debug/.build-id/75/24e9d6f20f5d7ef31f00709d236ac8c01817e1.debug

No differences were encountered between the control files of package vifm

Control files of package vifm-dbgsym: lines which differ (wdiff format)

  • Build-Ids: 7524e9d6f20f5d7ef31f00709d236ac8c01817e1 b5d9f0e27028e6d597d7707b86b69db88f56fe1a

More details

Full run details