From 31204af9c8b1e2eb52aebe141f14024e54266e7d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?= <deso@posteo.net>
Date: Sun, 8 Feb 2026 19:43:26 -0800
Subject: [PATCH] libreadline

---
 build-aux/config.h.in  |   3 +
 configure              |  91 +++++++++
 configure.ac           |  19 ++
 src/Makefile.am        |   1 +
 src/Makefile.in        |  74 +++----
 src/event_loop.c       |  46 +++++
 src/modes/cmdline.c    | 120 ++++++++++++
 src/modes/cmdline.h    |  35 ++++
 src/modes/cmdline_rl.c | 424 +++++++++++++++++++++++++++++++++++++++++
 src/modes/cmdline_rl.h |  53 ++++++
 10 files changed, 832 insertions(+), 34 deletions(-)
 create mode 100644 src/modes/cmdline_rl.c
 create mode 100644 src/modes/cmdline_rl.h

diff --git a/build-aux/config.h.in b/build-aux/config.h.in
index 8dfa89..e31466 100644
--- a/build-aux/config.h.in
+++ b/build-aux/config.h.in
@@ -80,6 +80,9 @@
 /* Define to 1 if you have the 'random' function. */
 #undef HAVE_RANDOM
 
+/* Define to 1 if you have libreadline. */
+#undef HAVE_READLINE
+
 /* Define to 1 if you have the 'reallocarray' function. */
 #undef HAVE_REALLOCARRAY
 
diff --git a/configure b/configure
index 0fa2c7..fb0908 100755
--- a/configure
+++ b/configure
@@ -775,6 +775,7 @@ with_gtk
 with_libmagic
 with_X11
 with_dyn_X11
+with_readline
 enable_extended_keys
 enable_desktop_files
 enable_remote_cmds
@@ -1459,6 +1460,8 @@ Optional Packages:
   --with-X11              use libX11 to determine terminal emulator title
                           [default=yes]
   --with-dyn-X11          load libX11 dynamically [default=yes]
+  --without-readline      disable libreadline for command-line editing
+                          [default=yes]
   --with-sanitize=basic|thread|leak
                           enables sanitizers, "basic" means address and
                           undefined sanitizers [default=]
@@ -11348,6 +11351,94 @@ fi
 fi
 
 
+# Check whether --with-readline was given.
+if test ${with_readline+y}
+then :
+  withval=$with_readline; use_readline=$withval
+else case e in #(
+  e) use_readline=yes ;;
+esac
+fi
+
+
+if test "$use_readline" = "yes"; then
+    { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for readline in -lreadline" >&5
+printf %s "checking for readline in -lreadline... " >&6; }
+if test ${ac_cv_lib_readline_readline+y}
+then :
+  printf %s "(cached) " >&6
+else case e in #(
+  e) ac_check_lib_save_LIBS=$LIBS
+LIBS="-lreadline  $LIBS"
+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.
+   The 'extern "C"' is for builds by C++ compilers;
+   although this is not generally supported in C code supporting it here
+   has little cost and some practical benefit (sr 110532).  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char readline (void);
+int
+main (void)
+{
+return readline ();
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+  ac_cv_lib_readline_readline=yes
+else case e in #(
+  e) ac_cv_lib_readline_readline=no ;;
+esac
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+    conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS ;;
+esac
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_readline_readline" >&5
+printf "%s\n" "$ac_cv_lib_readline_readline" >&6; }
+if test "x$ac_cv_lib_readline_readline" = xyes
+then :
+  LIBS="$LIBS -lreadline"
+         ac_fn_c_check_header_compile "$LINENO" "readline/readline.h" "ac_cv_header_readline_readline_h" "$ac_includes_default"
+if test "x$ac_cv_header_readline_readline_h" = xyes
+then :
+
+else case e in #(
+  e) as_fn_error $? "readline/readline.h not found." "$LINENO" 5 ;;
+esac
+fi
+
+         ac_fn_c_check_header_compile "$LINENO" "readline/history.h" "ac_cv_header_readline_history_h" "$ac_includes_default"
+if test "x$ac_cv_header_readline_history_h" = xyes
+then :
+
+else case e in #(
+  e) as_fn_error $? "readline/history.h not found." "$LINENO" 5 ;;
+esac
+fi
+
+
+printf "%s\n" "#define HAVE_READLINE 1" >>confdefs.h
+
+else case e in #(
+  e) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: libreadline not found, building without readline support." >&5
+printf "%s\n" "$as_me: WARNING: libreadline not found, building without readline support." >&2;}
+         use_readline=no ;;
+esac
+fi
+
+fi
+
+
 # Check whether --enable-extended_keys was given.
 if test ${enable_extended_keys+y}
 then :
diff --git a/configure.ac b/configure.ac
index 90f803..e575df 100644
--- a/configure.ac
+++ b/configure.ac
@@ -706,6 +706,25 @@ if test "$use_libmagic" = "yes"; then
          AC_DEFINE([HAVE_LIBMAGIC], [1], [Define to 1 if you have the `magic' library (-lmagic).])])
 fi
 
+AC_ARG_WITH(readline,
+    AS_HELP_STRING([--without-readline],
+        [disable libreadline for command-line editing @<:@default=yes@:>@]),
+    [use_readline=$withval],
+    [use_readline=yes])
+
+if test "$use_readline" = "yes"; then
+    AC_CHECK_LIB(readline, readline,
+        [LIBS="$LIBS -lreadline"
+         AC_CHECK_HEADER([readline/readline.h], [],
+             [AC_MSG_ERROR([readline/readline.h not found.])])
+         AC_CHECK_HEADER([readline/history.h], [],
+             [AC_MSG_ERROR([readline/history.h not found.])])
+         AC_DEFINE([HAVE_READLINE], [1],
+             [Define to 1 if you have libreadline.])],
+        [AC_MSG_WARN([libreadline not found, building without readline support.])
+         use_readline=no])
+fi
+
 dnl ----------------------------------------------------------------------------
 dnl handle options
 dnl ----------------------------------------------------------------------------
diff --git a/src/Makefile.am b/src/Makefile.am
index e83956..ceee20 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -243,6 +243,7 @@ vifm_SOURCES = \
 	modes/dialogs/sort_dialog.c modes/dialogs/sort_dialog.h \
 	\
 	modes/cmdline.c modes/cmdline.h \
+	modes/cmdline_rl.c modes/cmdline_rl.h \
 	modes/file_info.c modes/file_info.h \
 	modes/menu.c modes/menu.h \
 	modes/modes.c modes/modes.h \
diff --git a/src/Makefile.in b/src/Makefile.in
index fc4d0a..2be23e 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -190,36 +190,36 @@ am_vifm_OBJECTS = cfg/config.$(OBJEXT) cfg/info.$(OBJEXT) \
 	modes/dialogs/change_dialog.$(OBJEXT) \
 	modes/dialogs/msg_dialog.$(OBJEXT) \
 	modes/dialogs/sort_dialog.$(OBJEXT) modes/cmdline.$(OBJEXT) \
-	modes/file_info.$(OBJEXT) modes/menu.$(OBJEXT) \
-	modes/modes.$(OBJEXT) modes/more.$(OBJEXT) \
-	modes/normal.$(OBJEXT) modes/view.$(OBJEXT) \
-	modes/visual.$(OBJEXT) ui/cancellation.$(OBJEXT) \
-	ui/color_manager.$(OBJEXT) ui/color_scheme.$(OBJEXT) \
-	ui/colored_line.$(OBJEXT) ui/column_view.$(OBJEXT) \
-	ui/escape.$(OBJEXT) ui/fileview.$(OBJEXT) \
-	ui/quickview.$(OBJEXT) ui/statusbar.$(OBJEXT) \
-	ui/statusline.$(OBJEXT) ui/tabs.$(OBJEXT) ui/ui.$(OBJEXT) \
-	utils/cancellation.$(OBJEXT) utils/dynarray.$(OBJEXT) \
-	utils/env.$(OBJEXT) utils/event_nix.$(OBJEXT) \
-	utils/file_streams.$(OBJEXT) utils/filemon.$(OBJEXT) \
-	utils/filter.$(OBJEXT) utils/fs.$(OBJEXT) \
-	utils/fsdata.$(OBJEXT) utils/fsddata.$(OBJEXT) \
-	utils/fswatch_nix.$(OBJEXT) utils/globs.$(OBJEXT) \
-	utils/gmux_nix.$(OBJEXT) utils/hist.$(OBJEXT) \
-	utils/int_stack.$(OBJEXT) utils/log.$(OBJEXT) \
-	utils/matcher.$(OBJEXT) utils/matchers.$(OBJEXT) \
-	utils/mem.$(OBJEXT) utils/parson.$(OBJEXT) \
-	utils/path.$(OBJEXT) utils/regexp.$(OBJEXT) \
-	utils/selector_nix.$(OBJEXT) utils/shmem_nix.$(OBJEXT) \
-	utils/str.$(OBJEXT) utils/string_array.$(OBJEXT) \
-	utils/trie.$(OBJEXT) utils/utf8.$(OBJEXT) \
-	utils/utf8proc.$(OBJEXT) utils/utils.$(OBJEXT) \
-	utils/utils_nix.$(OBJEXT) args.$(OBJEXT) background.$(OBJEXT) \
-	bmarks.$(OBJEXT) bracket_notation.$(OBJEXT) \
-	builtin_functions.$(OBJEXT) cmd_actions.$(OBJEXT) \
-	cmd_completion.$(OBJEXT) cmd_core.$(OBJEXT) \
-	cmd_handlers.$(OBJEXT) compare.$(OBJEXT) dir_stack.$(OBJEXT) \
-	event_loop.$(OBJEXT) filelist.$(OBJEXT) \
+	modes/cmdline_rl.$(OBJEXT) modes/file_info.$(OBJEXT) \
+	modes/menu.$(OBJEXT) modes/modes.$(OBJEXT) \
+	modes/more.$(OBJEXT) modes/normal.$(OBJEXT) \
+	modes/view.$(OBJEXT) modes/visual.$(OBJEXT) \
+	ui/cancellation.$(OBJEXT) ui/color_manager.$(OBJEXT) \
+	ui/color_scheme.$(OBJEXT) ui/colored_line.$(OBJEXT) \
+	ui/column_view.$(OBJEXT) ui/escape.$(OBJEXT) \
+	ui/fileview.$(OBJEXT) ui/quickview.$(OBJEXT) \
+	ui/statusbar.$(OBJEXT) ui/statusline.$(OBJEXT) \
+	ui/tabs.$(OBJEXT) ui/ui.$(OBJEXT) utils/cancellation.$(OBJEXT) \
+	utils/dynarray.$(OBJEXT) utils/env.$(OBJEXT) \
+	utils/event_nix.$(OBJEXT) utils/file_streams.$(OBJEXT) \
+	utils/filemon.$(OBJEXT) utils/filter.$(OBJEXT) \
+	utils/fs.$(OBJEXT) utils/fsdata.$(OBJEXT) \
+	utils/fsddata.$(OBJEXT) utils/fswatch_nix.$(OBJEXT) \
+	utils/globs.$(OBJEXT) utils/gmux_nix.$(OBJEXT) \
+	utils/hist.$(OBJEXT) utils/int_stack.$(OBJEXT) \
+	utils/log.$(OBJEXT) utils/matcher.$(OBJEXT) \
+	utils/matchers.$(OBJEXT) utils/mem.$(OBJEXT) \
+	utils/parson.$(OBJEXT) utils/path.$(OBJEXT) \
+	utils/regexp.$(OBJEXT) utils/selector_nix.$(OBJEXT) \
+	utils/shmem_nix.$(OBJEXT) utils/str.$(OBJEXT) \
+	utils/string_array.$(OBJEXT) utils/trie.$(OBJEXT) \
+	utils/utf8.$(OBJEXT) utils/utf8proc.$(OBJEXT) \
+	utils/utils.$(OBJEXT) utils/utils_nix.$(OBJEXT) args.$(OBJEXT) \
+	background.$(OBJEXT) bmarks.$(OBJEXT) \
+	bracket_notation.$(OBJEXT) builtin_functions.$(OBJEXT) \
+	cmd_actions.$(OBJEXT) cmd_completion.$(OBJEXT) \
+	cmd_core.$(OBJEXT) cmd_handlers.$(OBJEXT) compare.$(OBJEXT) \
+	dir_stack.$(OBJEXT) event_loop.$(OBJEXT) filelist.$(OBJEXT) \
 	filename_modifiers.$(OBJEXT) fops_common.$(OBJEXT) \
 	fops_cpmv.$(OBJEXT) fops_misc.$(OBJEXT) fops_put.$(OBJEXT) \
 	fops_rename.$(OBJEXT) filetype.$(OBJEXT) filtering.$(OBJEXT) \
@@ -358,10 +358,10 @@ am__depfiles_remade = ./$(DEPDIR)/args.Po ./$(DEPDIR)/background.Po \
 	menus/$(DEPDIR)/trash_menu.Po menus/$(DEPDIR)/trashes_menu.Po \
 	menus/$(DEPDIR)/undolist_menu.Po menus/$(DEPDIR)/users_menu.Po \
 	menus/$(DEPDIR)/vifm_menu.Po modes/$(DEPDIR)/cmdline.Po \
-	modes/$(DEPDIR)/file_info.Po modes/$(DEPDIR)/menu.Po \
-	modes/$(DEPDIR)/modes.Po modes/$(DEPDIR)/more.Po \
-	modes/$(DEPDIR)/normal.Po modes/$(DEPDIR)/view.Po \
-	modes/$(DEPDIR)/visual.Po \
+	modes/$(DEPDIR)/cmdline_rl.Po modes/$(DEPDIR)/file_info.Po \
+	modes/$(DEPDIR)/menu.Po modes/$(DEPDIR)/modes.Po \
+	modes/$(DEPDIR)/more.Po modes/$(DEPDIR)/normal.Po \
+	modes/$(DEPDIR)/view.Po modes/$(DEPDIR)/visual.Po \
 	modes/dialogs/$(DEPDIR)/attr_dialog_nix.Po \
 	modes/dialogs/$(DEPDIR)/change_dialog.Po \
 	modes/dialogs/$(DEPDIR)/msg_dialog.Po \
@@ -969,6 +969,7 @@ vifm_SOURCES = \
 	modes/dialogs/sort_dialog.c modes/dialogs/sort_dialog.h \
 	\
 	modes/cmdline.c modes/cmdline.h \
+	modes/cmdline_rl.c modes/cmdline_rl.h \
 	modes/file_info.c modes/file_info.h \
 	modes/menu.c modes/menu.h \
 	modes/modes.c modes/modes.h \
@@ -1478,6 +1479,8 @@ modes/$(DEPDIR)/$(am__dirstamp):
 	@: >>modes/$(DEPDIR)/$(am__dirstamp)
 modes/cmdline.$(OBJEXT): modes/$(am__dirstamp) \
 	modes/$(DEPDIR)/$(am__dirstamp)
+modes/cmdline_rl.$(OBJEXT): modes/$(am__dirstamp) \
+	modes/$(DEPDIR)/$(am__dirstamp)
 modes/file_info.$(OBJEXT): modes/$(am__dirstamp) \
 	modes/$(DEPDIR)/$(am__dirstamp)
 modes/menu.$(OBJEXT): modes/$(am__dirstamp) \
@@ -1839,6 +1842,7 @@ distclean-compile:
 @AMDEP_TRUE@@am__include@ @am__quote@menus/$(DEPDIR)/users_menu.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@menus/$(DEPDIR)/vifm_menu.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@modes/$(DEPDIR)/cmdline.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@modes/$(DEPDIR)/cmdline_rl.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@modes/$(DEPDIR)/file_info.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@modes/$(DEPDIR)/menu.Po@am__quote@ # am--include-marker
 @AMDEP_TRUE@@am__include@ @am__quote@modes/$(DEPDIR)/modes.Po@am__quote@ # am--include-marker
@@ -2907,6 +2911,7 @@ distclean: distclean-am
 	-rm -f menus/$(DEPDIR)/users_menu.Po
 	-rm -f menus/$(DEPDIR)/vifm_menu.Po
 	-rm -f modes/$(DEPDIR)/cmdline.Po
+	-rm -f modes/$(DEPDIR)/cmdline_rl.Po
 	-rm -f modes/$(DEPDIR)/file_info.Po
 	-rm -f modes/$(DEPDIR)/menu.Po
 	-rm -f modes/$(DEPDIR)/modes.Po
@@ -3176,6 +3181,7 @@ maintainer-clean: maintainer-clean-am
 	-rm -f menus/$(DEPDIR)/users_menu.Po
 	-rm -f menus/$(DEPDIR)/vifm_menu.Po
 	-rm -f modes/$(DEPDIR)/cmdline.Po
+	-rm -f modes/$(DEPDIR)/cmdline_rl.Po
 	-rm -f modes/$(DEPDIR)/file_info.Po
 	-rm -f modes/$(DEPDIR)/menu.Po
 	-rm -f modes/$(DEPDIR)/modes.Po
diff --git a/src/event_loop.c b/src/event_loop.c
index 8c9fe4..802728 100644
--- a/src/event_loop.c
+++ b/src/event_loop.c
@@ -39,6 +39,10 @@
 #include "modes/dialogs/msg_dialog.h"
 #include "modes/modes.h"
 #include "modes/wk.h"
+#ifdef HAVE_READLINE
+#include "modes/cmdline.h"
+#include "modes/cmdline_rl.h"
+#endif
 #include "ui/fileview.h"
 #include "ui/quickview.h"
 #include "ui/statusbar.h"
@@ -218,6 +222,48 @@ event_loop(const int *quit, int manage_marking)
 				continue;
 			}
 
+#ifdef HAVE_READLINE
+			if(modes_is_cmdline_like() && cmdline_rl_active()
+					&& !modcline_is_navigating())
+			{
+				cmdline_rl_feed(c);
+
+				if(cmdline_rl_accepted())
+				{
+					modcline_accept_input();
+				}
+				else if(cmdline_rl_cancelled())
+				{
+					modcline_cancel_input();
+				}
+
+				if(cmdline_rl_accepted() || cmdline_rl_cancelled())
+				{
+					/* Match the normal post-command processing flow. */
+					process_scheduled_updates();
+					reset_input_buf(input_buf, &input_buf_pos);
+					modes_input_bar_clear();
+
+					if(ui_sb_multiline())
+					{
+						wait_for_enter = 1;
+						update_all_windows();
+						continue;
+					}
+
+					(void)vifm_chdir(flist_get_dir(curr_view));
+					modes_post();
+					continue;
+				}
+
+				/* Normal character — just update and continue. */
+				process_scheduled_updates();
+				reset_input_buf(input_buf, &input_buf_pos);
+				modes_input_bar_clear();
+				continue;
+			}
+#endif
+
 			int drop_all_input = 0;
 
 			if(input_buf_pos < ARRAY_LEN(input_buf) - 2)
diff --git a/src/modes/cmdline.c b/src/modes/cmdline.c
index 0b1fda..cb2549 100644
--- a/src/modes/cmdline.c
+++ b/src/modes/cmdline.c
@@ -76,6 +76,10 @@
 #include "visual.h"
 #include "wk.h"
 
+#ifdef HAVE_READLINE
+#include "cmdline_rl.h"
+#endif
+
 /* Prompt prefix when navigation is enabled. */
 #define NAV_PREFIX L"(nav)"
 
@@ -322,6 +326,10 @@ modcline_init(void)
 	assert(ret_code == 0);
 
 	(void)ret_code;
+
+#ifdef HAVE_READLINE
+	cmdline_rl_init();
+#endif
 }
 
 void
@@ -342,6 +350,20 @@ def_handler(wchar_t key)
 	void *p;
 	wchar_t buf[2] = {key, L'\0'};
 
+#ifdef HAVE_READLINE
+	/* When readline is active, characters that arrive through the key engine
+	 * (e.g. from mapping expansion like nnoremap <f7> :mkdir<space>) must be
+	 * fed to readline so its buffer stays in sync with input_stat.  Without
+	 * this, readline's buffer would remain empty while input_stat.line is
+	 * modified directly, causing the next character from the event loop to
+	 * wipe the text when readline syncs its state back. */
+	if(cmdline_rl_active() && !input_stat.navigating)
+	{
+		cmdline_rl_feed((wint_t)key);
+		return 0;
+	}
+#endif
+
 	input_stat.hist_search = HIST_NONE;
 
 	if(input_stat.complete_continue && !input_stat.inc_completion
@@ -914,6 +936,10 @@ prepare_cmdline_mode(const wchar_t prompt[], const wchar_t initial[],
 
 	/* Make cursor visible only after all initial draws. */
 	ui_set_cursor(/*visibility=*/1);
+
+#ifdef HAVE_READLINE
+	cmdline_rl_start();
+#endif
 }
 
 /* Initializes command-line status. */
@@ -1023,6 +1049,10 @@ is_line_edited(void)
 static void
 leave_cmdline_mode(int cancelled)
 {
+#ifdef HAVE_READLINE
+	cmdline_rl_stop();
+#endif
+
 	free_line_stats(&input_stat);
 
 	if(is_cmdmode(vle_mode_get()))
@@ -3548,5 +3578,95 @@ handle_mouse_event(key_info_t key_info, keys_info_t *keys_info)
 	}
 }
 
+#ifdef HAVE_READLINE
+
+void
+modcline_sync_from_readline(const char mb_line[], int char_index)
+{
+	wchar_t *wide_line = to_wide(mb_line);
+	if(wide_line == NULL)
+	{
+		return;
+	}
+
+	free(input_stat.line);
+	input_stat.line = wide_line;
+	input_stat.len = (int)wcslen(wide_line);
+	input_stat.index = (char_index <= input_stat.len) ? char_index
+	                                                  : input_stat.len;
+	input_stat.curs_pos = input_stat.prompt_wid
+	                    + esc_wcswidth(input_stat.line, input_stat.index);
+
+	update_cmdline_size();
+	update_cmdline_text(&input_stat);
+}
+
+void
+modcline_accept_input(void)
+{
+	key_info_t key_info = {};
+	keys_info_t keys_info = {};
+	cmd_return(key_info, &keys_info);
+}
+
+void
+modcline_cancel_input(void)
+{
+	key_info_t key_info = {};
+	keys_info_t keys_info = {};
+	cmd_ctrl_c(key_info, &keys_info);
+}
+
+void
+modcline_do_completion(void)
+{
+	do_completion();
+}
+
+int
+modcline_is_navigating(void)
+{
+	return input_stat.navigating;
+}
+
+void
+modcline_get_prompt(char buf[], size_t buf_size)
+{
+	wcstombs(buf, input_stat.prompt, buf_size);
+	buf[buf_size - 1] = '\0';
+}
+
+void
+modcline_get_initial(char buf[], size_t buf_size)
+{
+	if(input_stat.line != NULL)
+	{
+		wcstombs(buf, input_stat.line, buf_size);
+		buf[buf_size - 1] = '\0';
+	}
+	else
+	{
+		buf[0] = '\0';
+	}
+}
+
+const hist_t *
+modcline_get_hist(void)
+{
+	return pick_hist();
+}
+
+char *
+modcline_get_line_mb(void)
+{
+	if(input_stat.line == NULL)
+	{
+		return NULL;
+	}
+	return to_multibyte(input_stat.line);
+}
+
+#endif /* HAVE_READLINE */
+
 /* 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 0572c9..c71025 100644
--- a/src/modes/cmdline.h
+++ b/src/modes/cmdline.h
@@ -85,6 +85,41 @@ 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);
 
+#ifdef HAVE_READLINE
+
+#include <stddef.h> /* size_t */
+
+#include "../utils/hist.h"
+
+/* Syncs Vifm's input state from readline's line buffer and cursor index. */
+void modcline_sync_from_readline(const char mb_line[], int char_index);
+
+/* Performs accept-input (Enter) logic. */
+void modcline_accept_input(void);
+
+/* Performs cancel-input (Ctrl+C) logic. */
+void modcline_cancel_input(void);
+
+/* Invokes Vifm's tab completion. */
+void modcline_do_completion(void);
+
+/* Returns non-zero if navigation mode is active. */
+int modcline_is_navigating(void);
+
+/* Retrieves the prompt as a multibyte string into buf. */
+void modcline_get_prompt(char buf[], size_t buf_size);
+
+/* Retrieves the initial line as a multibyte string into buf. */
+void modcline_get_initial(char buf[], size_t buf_size);
+
+/* Returns current history for the active submode, or NULL. */
+const hist_t *modcline_get_hist(void);
+
+/* Returns the current line as a newly allocated multibyte string, or NULL. */
+char *modcline_get_line_mb(void);
+
+#endif /* HAVE_READLINE */
+
 #if defined(TEST) || defined(CMDLINE_IMPL)
 
 #include <stddef.h> /* size_t wchar_t */
diff --git a/src/modes/cmdline_rl.c b/src/modes/cmdline_rl.c
new file mode 100644
index 000000..960563
--- /dev/null
+++ b/src/modes/cmdline_rl.c
@@ -0,0 +1,424 @@
+/* vifm
+ * Copyright (C) 2001 Ken Steen.
+ * Copyright (C) 2011 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 "cmdline_rl.h"
+
+#ifdef HAVE_READLINE
+
+#include <curses.h>
+
+#include <readline/readline.h>
+#include <readline/history.h>
+
+#include <stdlib.h> /* free() malloc() */
+#include <string.h> /* strlen() */
+#include <wchar.h> /* wchar_t wcslen() mbstowcs() wcstombs() wctomb() */
+
+#include "../compat/curses.h"
+#include "../compat/reallocarray.h"
+#include "../utils/hist.h"
+#include "../utils/macros.h"
+#include "../utils/str.h"
+#include "../status.h"
+#include "cmdline.h"
+
+/* Size of the internal getc buffer for feeding bytes to readline. */
+#define GETC_BUF_SIZE 64
+
+/* Whether readline is actively handling input. */
+static int rl_active;
+
+/* Set when Enter is pressed (accepted). */
+static int line_accepted;
+
+/* Set when Ctrl+C cancels input. */
+static int cancelled;
+
+/* Buffer for feeding bytes to readline's rl_getc_function. */
+static unsigned char getc_buf[GETC_BUF_SIZE];
+static int getc_buf_pos;
+static int getc_buf_len;
+
+static void noop_prep(int meta_flag);
+static void noop_deprep(void);
+static int vifm_rl_input_available(void);
+static int vifm_rl_getc(FILE *stream);
+static void vifm_rl_redisplay(void);
+static void vifm_rl_accepted_cb(char *line);
+static int vifm_rl_cancel(int count, int key);
+static char **vifm_rl_completion(const char *text, int start, int end);
+static char *vifm_rl_compentry(const char *text, int state);
+static void load_vifm_history(const hist_t *hist);
+static const char *ncurses_key_to_seq(int ncurses_key);
+static int byte_offset_to_char_index(const char *s, int byte_off);
+
+static void
+noop_prep(int meta_flag)
+{
+	(void)meta_flag;
+}
+
+static void
+noop_deprep(void)
+{
+}
+
+void
+cmdline_rl_init(void)
+{
+	/* Prevent readline from touching the terminal — Vifm/ncurses owns it. */
+	rl_prep_term_function = noop_prep;
+	rl_deprep_term_function = noop_deprep;
+	rl_catch_signals = 0;
+	rl_catch_sigwinch = 0;
+	rl_change_environment = 0;
+
+	/* Use our own rendering and character source. */
+	rl_redisplay_function = vifm_rl_redisplay;
+	rl_getc_function = vifm_rl_getc;
+	rl_input_available_hook = vifm_rl_input_available;
+
+	rl_initialize();
+
+	/* Bind Ctrl+C to cancel. */
+	rl_bind_key(CTRL('C'), vifm_rl_cancel);
+
+	/* Use Vifm's completion system. */
+	rl_attempted_completion_function = vifm_rl_completion;
+
+	/* Disable readline's built-in filename completion fallback. */
+	rl_completion_entry_function = vifm_rl_compentry;
+}
+
+void
+cmdline_rl_start(void)
+{
+	char mb_prompt[NAME_MAX + 1];
+	char mb_initial[4096];
+
+	line_accepted = 0;
+	cancelled = 0;
+	getc_buf_pos = 0;
+	getc_buf_len = 0;
+
+	/* Convert prompt and initial text BEFORE rl_callback_handler_install(),
+	 * because that function internally calls rl_redisplay(), which triggers our
+	 * vifm_rl_redisplay() callback that syncs the (empty) rl_line_buffer back
+	 * to input_stat.line, wiping the initial text. */
+	modcline_get_prompt(mb_prompt, sizeof(mb_prompt));
+	modcline_get_initial(mb_initial, sizeof(mb_initial));
+
+	rl_callback_handler_install(mb_prompt, vifm_rl_accepted_cb);
+
+	/* If there's initial text, insert it into readline's buffer. */
+	if(mb_initial[0] != '\0')
+	{
+		rl_insert_text(mb_initial);
+		vifm_rl_redisplay();
+	}
+
+	/* Load appropriate history. */
+	const hist_t *hist = modcline_get_hist();
+	if(hist != NULL)
+	{
+		load_vifm_history(hist);
+	}
+
+	rl_active = 1;
+}
+
+void
+cmdline_rl_stop(void)
+{
+	rl_callback_handler_remove();
+	clear_history();
+	rl_active = 0;
+	line_accepted = 0;
+	cancelled = 0;
+}
+
+int
+cmdline_rl_active(void)
+{
+	return rl_active;
+}
+
+void
+cmdline_rl_feed(wint_t c)
+{
+	line_accepted = 0;
+	cancelled = 0;
+	getc_buf_pos = 0;
+	getc_buf_len = 0;
+
+	/* Check if this is a K()-wrapped ncurses functional key. */
+	if(c >= (wint_t)0xe001)
+	{
+		int ncurses_key = (int)(c - (wint_t)0xe001);
+		const char *seq = ncurses_key_to_seq(ncurses_key);
+		if(seq != NULL)
+		{
+			while(*seq != '\0' && getc_buf_len < GETC_BUF_SIZE)
+			{
+				getc_buf[getc_buf_len++] = (unsigned char)*seq++;
+			}
+		}
+		else
+		{
+			/* Unknown functional key — ignore. */
+			return;
+		}
+	}
+	else
+	{
+		/* Regular character — convert wchar_t to multibyte. */
+		char mb[MB_LEN_MAX];
+		int len = wctomb(mb, (wchar_t)c);
+		if(len > 0)
+		{
+			int i;
+			for(i = 0; i < len && getc_buf_len < GETC_BUF_SIZE; i++)
+			{
+				getc_buf[getc_buf_len++] = (unsigned char)mb[i];
+			}
+		}
+		else
+		{
+			return;
+		}
+	}
+
+	/* Save readline state before processing to detect no-op Esc. */
+	const int old_point = rl_point;
+	const int old_end = rl_end;
+
+	/* Feed all buffered bytes to readline. */
+	while(getc_buf_pos < getc_buf_len && !line_accepted && !cancelled)
+	{
+		rl_callback_read_char();
+	}
+
+	/* Detect Esc-as-cancel: if Esc was pressed and readline's cursor and line
+	 * length didn't change, treat it as cancel.  In Vi mode the first Esc
+	 * switches from insert to command mode (cursor moves), so only the second
+	 * Esc (which is a no-op) triggers cancel.  In Emacs mode Esc alone just
+	 * sets the Meta prefix without changing anything, so it cancels
+	 * immediately — users can use Alt+key for Meta combinations instead. */
+	if(c == L'\x1b' && !line_accepted && !cancelled
+			&& rl_point == old_point && rl_end == old_end)
+	{
+		cancelled = 1;
+	}
+}
+
+int
+cmdline_rl_accepted(void)
+{
+	return line_accepted;
+}
+
+int
+cmdline_rl_cancelled(void)
+{
+	return cancelled;
+}
+
+/* Readline's input-availability hook — tells readline whether our internal
+ * buffer has data.  Without this, readline checks stdin directly via
+ * select()/poll() to decide if more bytes follow an Esc (for escape sequence
+ * disambiguation).  Since we feed characters through our own buffer, not
+ * stdin, readline's default check can see stale terminal response bytes and
+ * misinterpret a standalone Esc as the start of a Meta sequence. */
+static int
+vifm_rl_input_available(void)
+{
+	return getc_buf_pos < getc_buf_len;
+}
+
+/* Readline's getc function — reads from our internal buffer.  Must never
+ * return EOF, because readline 8.x calls getc twice per
+ * rl_callback_read_char() (once for the character, once for lookahead) and
+ * interprets EOF as end-of-input, firing the handler prematurely.  Returning
+ * 0 (NUL) when the buffer is empty is harmless — readline treats it as a
+ * no-op character. */
+static int
+vifm_rl_getc(FILE *stream)
+{
+	(void)stream;
+
+	if(getc_buf_pos < getc_buf_len)
+	{
+		return getc_buf[getc_buf_pos++];
+	}
+
+	return 0;
+}
+
+/* Readline's redisplay callback — syncs readline state to Vifm.  Skipped
+ * after the line has been accepted or cancelled, because readline 8.x
+ * re-initializes its line buffer after calling the handler and triggers
+ * another redisplay with an empty buffer, which would wipe input_stat. */
+static void
+vifm_rl_redisplay(void)
+{
+	if(line_accepted || cancelled)
+	{
+		return;
+	}
+
+	int char_index = byte_offset_to_char_index(rl_line_buffer, rl_point);
+	modcline_sync_from_readline(rl_line_buffer, char_index);
+}
+
+/* Readline's accept-line callback (Enter pressed).  Also called by readline
+ * when our cancel function sets rl_done, so check the cancelled flag first. */
+static void
+vifm_rl_accepted_cb(char *line)
+{
+	(void)line;
+	if(!cancelled)
+	{
+		line_accepted = 1;
+	}
+}
+
+/* Custom readline function bound to Ctrl+C. */
+static int
+vifm_rl_cancel(int count, int key)
+{
+	(void)count;
+	(void)key;
+	rl_done = 1;
+	cancelled = 1;
+	return 0;
+}
+
+/* Readline completion function — bridges to Vifm's completion. */
+static char **
+vifm_rl_completion(const char *text, int start, int end)
+{
+	(void)text;
+	(void)start;
+	(void)end;
+
+	/* Suppress readline's default filename completion. */
+	rl_attempted_completion_over = 1;
+
+	/* Trigger Vifm's completion, which updates input_stat directly. */
+	modcline_do_completion();
+
+	/* After Vifm's completion updates input_stat, sync back to readline. */
+	char *mb_line = modcline_get_line_mb();
+	if(mb_line != NULL)
+	{
+		rl_replace_line(mb_line, 0);
+		rl_point = (int)strlen(mb_line);
+		free(mb_line);
+	}
+
+	/* Return NULL — we've handled the completion ourselves by updating
+	 * the line buffer directly. */
+	return NULL;
+}
+
+/* Dummy completion entry function to suppress readline's default filename
+ * completion. */
+static char *
+vifm_rl_compentry(const char *text, int state)
+{
+	(void)text;
+	(void)state;
+	return NULL;
+}
+
+/* Populates readline's history from a Vifm history. */
+static void
+load_vifm_history(const hist_t *hist)
+{
+	int i;
+
+	clear_history();
+
+	/* Add oldest first so newest is at the top. */
+	for(i = hist->size - 1; i >= 0; i--)
+	{
+		if(hist->items[i].text != NULL)
+		{
+			add_history(hist->items[i].text);
+		}
+	}
+}
+
+/* Translates ncurses KEY_* constants to terminal escape sequences. */
+static const char *
+ncurses_key_to_seq(int ncurses_key)
+{
+	switch(ncurses_key)
+	{
+		case KEY_UP:        return "\033[A";
+		case KEY_DOWN:      return "\033[B";
+		case KEY_RIGHT:     return "\033[C";
+		case KEY_LEFT:      return "\033[D";
+		case KEY_HOME:      return "\033[H";
+		case KEY_END:       return "\033[F";
+		case KEY_DC:        return "\033[3~";
+		case KEY_BACKSPACE: return "\177";
+		case KEY_PPAGE:     return "\033[5~";
+		case KEY_NPAGE:     return "\033[6~";
+		default:            return NULL;
+	}
+}
+
+/* Converts a byte offset in a multibyte string to a character index. */
+static int
+byte_offset_to_char_index(const char *s, int byte_off)
+{
+	int chars = 0;
+	int bytes = 0;
+	mbstate_t state;
+	memset(&state, 0, sizeof(state));
+
+	while(bytes < byte_off && s[bytes] != '\0')
+	{
+		size_t len = mbrlen(&s[bytes], (size_t)(byte_off - bytes), &state);
+		if(len == (size_t)-1 || len == (size_t)-2)
+		{
+			/* Invalid or incomplete sequence — count as one byte/char. */
+			bytes++;
+			chars++;
+			memset(&state, 0, sizeof(state));
+		}
+		else if(len == 0)
+		{
+			break;
+		}
+		else
+		{
+			bytes += (int)len;
+			chars++;
+		}
+	}
+
+	return chars;
+}
+
+#endif /* HAVE_READLINE */
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 filetype=c : */
diff --git a/src/modes/cmdline_rl.h b/src/modes/cmdline_rl.h
new file mode 100644
index 000000..b432f4
--- /dev/null
+++ b/src/modes/cmdline_rl.h
@@ -0,0 +1,53 @@
+/* vifm
+ * Copyright (C) 2001 Ken Steen.
+ * Copyright (C) 2011 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__MODES__CMDLINE_RL_H__
+#define VIFM__MODES__CMDLINE_RL_H__
+
+#ifdef HAVE_READLINE
+
+#include <wchar.h> /* wint_t */
+
+/* Initializes readline integration once at startup. */
+void cmdline_rl_init(void);
+
+/* Starts a readline session when entering command-line mode. */
+void cmdline_rl_start(void);
+
+/* Stops the readline session when leaving command-line mode. */
+void cmdline_rl_stop(void);
+
+/* Returns non-zero if readline is currently handling input. */
+int cmdline_rl_active(void);
+
+/* Feeds one character from the event loop to readline. */
+void cmdline_rl_feed(wint_t c);
+
+/* Returns non-zero if line was accepted (Enter pressed). */
+int cmdline_rl_accepted(void);
+
+/* Returns non-zero if input was cancelled (Ctrl+C pressed). */
+int cmdline_rl_cancelled(void);
+
+#endif /* HAVE_READLINE */
+
+#endif /* VIFM__MODES__CMDLINE_RL_H__ */
+
+/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
+/* vim: set cinoptions+=t0 filetype=c : */
-- 
2.52.0

