/*
 * This file is part of Clifm
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 * SPDX-FileCopyrightText: 2016-2026 L. Abramovich <leo.clifm@outlook.com>
*/

/* search.c -- functions for the search system */

#include "helpers.h"

#include <errno.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#ifdef __sun
# include <sys/termios.h> /* TIOCGWINSZ */
#endif /* __sun */

/* We need rl_line_buffer in case of no matches and no metacharacter */
#include <readline/readline.h>

#include "aux.h"
#include "checks.h"
#include "colors.h"
#include "messages.h"
#include "misc.h"
#include "navigation.h"
#include "sort.h"
#include "spawn.h"

#define ERR_SKIP_REGEX 2

#if (defined(__OpenBSD__) || defined(__sun)) && !defined(_BE_POSIX)
/* OpenBSD/Solaris find(1) has neither -regex nor -iregex. We'll try to use
 * gfind(1) instead.*/
# define FIND_HAS_NO_REGEX
#endif /* __OpenBSD__ || __sun */

struct search_t {
	char *name;
	size_t len;
	int eln;
	int pad;
};

static int
exec_find(const char *name, const char *_path, const char *method, const char *pattern)
{
	if (conf.follow_symlinks == 1) {
		const char *cmd[] = {name, "-L", _path, method, pattern, NULL};
		return launch_execv(cmd, FOREGROUND, E_NOSTDERR);
	}

	const char *cmd[] = {name, _path, method, pattern, NULL};
	return launch_execv(cmd, FOREGROUND, E_NOSTDERR);
}

#ifdef FIND_HAS_NO_REGEX
static char *
define_find_name(void)
{
	static int check = 1;
	static int have_gfind = 0;

	/* Let's run this only once. */
	if (check == 1) {
		check = 0;
		if (is_cmd_in_path("gfind", NULL) == 1)
			have_gfind = 1;
	}

	return (have_gfind == 1 ? "gfind" : "find");
}
#endif /* FIND_HAS_NO_REGEX */

static int
run_find(char *search_path, char *arg)
{
	char *_path = (search_path && *search_path) ? search_path : ".";
	char *name = "find";

#if defined(_BE_POSIX)
	/* POSIX find(1) only supports -name */
	char *method = "-name";
#else
	char *method = conf.search_strategy == REGEX_ONLY
		? (conf.case_sens_search == 1 ? "-regex" : "-iregex")
		: (conf.case_sens_search == 1 ? "-name" : "-iname");
# if defined(FIND_HAS_NO_REGEX)
	name = define_find_name();
	if (*name != 'g') /* GNU find (gfind) not found */
		method = conf.case_sens_search == 1 ? "-name" : "-iname";
# endif /* FIND_HAS_NO_REGEX */
#endif /* _BE_POSIX */

	const int glob_char = check_glob_char(arg + 1, GLOB_REGEX);
	if (glob_char == 1)
		return exec_find(name, _path, method, arg + 1);

	const size_t pattern_len = strlen(arg + 1) + 5;
	char *pattern = xnmalloc(pattern_len, sizeof(char));

#if !defined(_BE_POSIX)
	if (conf.search_strategy == REGEX_ONLY) {
# if !defined(FIND_HAS_NO_REGEX)
		snprintf(pattern, pattern_len, ".*%s.*", arg + 1);
# else
		if (*name == 'g') /* We have GNU find (gfind) */
			snprintf(pattern, pattern_len, ".*%s.*", arg + 1);
		else
			snprintf(pattern, pattern_len, "*%s*", arg + 1);
# endif /* FIND_HAS_NO_REGEX */
	} else {
		snprintf(pattern, pattern_len, "*%s*", arg + 1);
	}
#else
	snprintf(pattern, pattern_len, "*%s*", arg + 1);
#endif /* !_BE_POSIX */

	const int ret = exec_find(name, _path, method, pattern);
	free(pattern);

	return ret;
}

static int
set_file_type_and_search_path(char **args, mode_t *file_type,
	char **search_path, const int invert)
{
	/* If there are two arguments, the one starting with '-' is the
	 * file type and the other is the path. */
	if (args[1] && args[2]) {
		if (*args[1] == '-') {
			*file_type = (mode_t)args[1][1];
			*search_path = args[2];
		} else if (*args[2] == '-') {
			*file_type = (mode_t)args[2][1];
			*search_path = args[1];
		} else {
			*search_path = args[1];
		}
	} else {
		/* If just one argument, '-' indicates file type. Else, we have a path. */
		if (args[1]) {
			if (*args[1] == '-')
				*file_type = (mode_t)args[1][1];
			else
				*search_path = args[1];
		}
	}

	if (*file_type == 0)
		return FUNC_SUCCESS;

	/* Convert file type into a macro that can be decoded by stat(). If
	 * file type is specified, matches will be checked against this value. */
	switch (*file_type) {
	case 'b': *file_type = invert == 1 ? DT_BLK : S_IFBLK; break;
	case 'c': *file_type = invert == 1 ? DT_CHR : S_IFCHR; break;
	case 'd': *file_type = invert == 1 ? DT_DIR : S_IFDIR; break;
#ifdef SOLARIS_DOORS
	case 'O': *file_type = invert == 1 ? DT_DOOR : S_IFDOOR; break;
	case 'P': *file_type = invert == 1 ? DT_PORT : S_IFPORT; break;
#endif /* SOLARIS_DOORS */
	case 'f': *file_type = invert == 1 ? DT_REG : S_IFREG; break;
	case 'l': *file_type = invert == 1 ? DT_LNK : S_IFLNK; break;
	case 'p': *file_type = invert == 1 ? DT_FIFO : S_IFIFO; break;
	case 's': *file_type = invert == 1 ? DT_SOCK : S_IFSOCK; break;
	case 'x': run_find(*search_path, args[0]); return FUNC_SUCCESS;
	default:
		fprintf(stderr, _("search: '%c': Unrecognized file "
			"type\n"), (char)*file_type);
		break;
	}

	return FUNC_SUCCESS;
}

static int
chdir_search_path(char **search_path, const char *arg)
{
	if (strchr(*search_path, '\\')) {
		char *deq_dir = unescape_str(*search_path);
		if (!deq_dir) {
			xerror(_("search: %s: Error unescaping filename\n"), arg);
			return FUNC_FAILURE;
		}

		xstrsncpy(*search_path, deq_dir, strlen(deq_dir) + 1);
		free(deq_dir);
	}

	const size_t path_len = strlen(*search_path);
	if (path_len > 1 && (*search_path)[path_len - 1] == '/')
		(*search_path)[path_len - 1] = '\0';

	/* If search path is the current directory. */
	if ((*(*search_path) == '.' && !(*search_path)[1]) ||
	    ((*search_path)[1] == workspaces[cur_ws].path[1]
	    && strcmp(*search_path, workspaces[cur_ws].path) == 0)) {
			*search_path = NULL;
	} else {
		if (xchdir(*search_path, NO_TITLE) == -1) {
			xerror("search: '%s': %s\n", *search_path, strerror(errno));
			return FUNC_FAILURE;
		}
	}

	return FUNC_SUCCESS;
}

static char **
glob_sort_dirs(glob_t *globbed_files, size_t *g)
{
	int *dirs = xnmalloc(globbed_files->gl_pathc + 1, sizeof(int));
	char **gfiles = xnmalloc(globbed_files->gl_pathc + 1, sizeof(char *));
	struct stat attr;
	size_t i, n = 0;

	for (i = 0; globbed_files->gl_pathv[i]; i++) {
		if (stat(globbed_files->gl_pathv[i], &attr) != -1
		&& S_ISDIR(attr.st_mode))
			dirs[i] = 1;
		else
			dirs[i] = 0;
	}

	for (i = 0; globbed_files->gl_pathv[i]; i++) {
		if (dirs[i] == 1)
			gfiles[n++] = globbed_files->gl_pathv[i];
	}

	for (i = 0; globbed_files->gl_pathv[i]; i++) {
		if (dirs[i] == 0)
			gfiles[n++] = globbed_files->gl_pathv[i];
	}

	free(dirs);
	gfiles[n] = NULL;

	*g = n;
	return gfiles;
}

static struct search_t *
get_glob_matches(char **gfiles, const char *search_path,
	const mode_t file_type, const size_t g)
{
	struct search_t *matches = xnmalloc(g + 1, sizeof(struct search_t));

	int n = 0;
	struct stat attr;

	for (size_t i = 0; gfiles[i]; i++) {
		if (SELFORPARENT(gfiles[i]))
			continue;

		if (file_type != 0) {
			/* Simply skip all files not matching file_type. */
			if (lstat(gfiles[i], &attr) == -1
			|| (attr.st_mode & S_IFMT) != file_type)
				continue;
		}

		matches[n].name = savestring(gfiles[i], strlen(gfiles[i]));

		/* Get the longest filename in the list. */
		/* If not in CWD, we only need to know the file's length (no ELN) */
		if (search_path) {
			/* This will be passed to colors_list(): -1 means no ELN */
			matches[n].eln = -1;
			matches[n].len = wc_xstrlen(matches[n].name);
			n++;
			continue;
		}

		/* No search_path */
		/* If searching in CWD, take into account the file's ELN
		 * when calculating its length. */
		size_t f = 0;
		for (size_t j = 0; file_info[j].name; j++) {
			if (!matches[n].name || *matches[n].name != *file_info[j].name
			|| strcmp(matches[n].name, file_info[j].name) != 0)
				continue;

			f = 1;
			matches[n].eln = (int)(j + 1);
			matches[n].len = wc_xstrlen(file_info[j].name)
				+ (size_t)file_info[j].eln_n + 1;
		}

		if (f == 0) {
			matches[n].eln = -1;
			matches[n].len = 0;
		}

		n++;
	}

	matches[n].name = NULL;
	return matches;
}

static struct search_t *
get_non_matches_from_search_path(const char *search_path, char **gfiles,
		const mode_t file_type)
{
	struct dirent **ent = NULL;
	int dir_entries = scandir(search_path, &ent, skip_files, xalphasort);
	if (dir_entries == -1)
		return NULL;

	int i, j, n = 0;
	struct search_t *matches = xnmalloc((size_t)dir_entries + 1,
		sizeof(struct search_t));

	for (i = 0; i < dir_entries; i++) {
		int f = 0;
		for (j = 0; gfiles[j]; j++) {
			if (*ent[i]->d_name == *gfiles[j]
			&& strcmp(ent[i]->d_name, gfiles[j]) == 0) {
				f = 1;
				break;
			}
		}

		if (f == 1)
			continue;

#if !defined(_DIRENT_HAVE_D_TYPE)
		struct stat attr;
		if (lstat(ent[i]->d_name, &attr) == -1)
			continue;

		const mode_t type = get_dt(attr.st_mode);
		if (file_type && type != file_type)
#else
		if (file_type && ent[i]->d_type != file_type)
#endif /* !_DIRENT_HAVE_D_TYPE */
			continue;

		matches[n].eln = -1;
		matches[n].len = wc_xstrlen(ent[i]->d_name);
		matches[n++].name = strdup(ent[i]->d_name);
	}

	i = dir_entries;
	while (--i >= 0)
		free(ent[i]);
	free(ent);

	matches[n].name = NULL;
	return matches;
}

static struct search_t *
get_glob_matches_invert(char **gfiles, const char *search_path,
	const mode_t file_type)
{
	if (search_path)
		return get_non_matches_from_search_path(search_path, gfiles, file_type);

	filesn_t n = 0;
	struct search_t *matches =
		xnmalloc((size_t)g_files_num + 1, sizeof(struct search_t));

	for (filesn_t i = 0; file_info[i].name; i++) {
		int f = 0;

		for (filesn_t j = 0; gfiles[j]; j++) {
			if (*gfiles[j] == *file_info[i].name
			&& strcmp(gfiles[j], file_info[i].name) == 0) {
				f = 1;
				break;
			}
		}

		if (f == 1 || (file_type && file_info[i].type != file_type))
			continue;

		matches[n].eln = (int)(i + 1);
		matches[n].len = wc_xstrlen(file_info[i].name)
			+ (size_t)file_info[i].eln_n + 1;

		matches[n++].name = strdup(file_info[i].name);
	}

	matches[n].name = NULL;
	return matches;
}

static void
get_glob_longest(struct search_t *matches, int *longest_eln,
	size_t *longest_match, int *eln_pad)
{
	int search_path = (*eln_pad == -1);

	for (size_t i = 0; matches[i].name; i++) {
		if (matches[i].len <= *longest_match)
			continue;

		*longest_match = matches[i].len;
		*longest_eln = matches[i].eln;
	}

	if (search_path == 1) {
		*longest_eln = -1;
		return;
	}

	if (conf.icons == 1)
		*longest_match += (size_t)ICON_LEN;

	int longest_name = 0;
	for (size_t i = 0; matches[i].name; i++) {
		if (matches[i].eln > longest_name)
			longest_name = matches[i].eln;
	}

	*eln_pad = DIGINUM(longest_name);
	*longest_match += (size_t)(*eln_pad - DIGINUM(*longest_eln));
}

/* Original string is either "/QUERY" or "/!QUERY". Let's extract QUERY.
 * If the query string contains no metacharacters, change it to "*QUERY*" */
static char *
construct_glob_query(char **arg, const int invert)
{
	search_flags &= ~NO_GLOB_CHAR;
	char *query = *arg + (invert == 1 ? 2 : 1);

	/* If the query string already contains metacharacters, return it as is. */
	if (check_glob_char(query, GLOB_REGEX) == 1)
		return query;

	search_flags |= NO_GLOB_CHAR;
	if (conf.search_strategy != GLOB_ONLY) {
		/* Let's return here to perform a regex search. */
		return NULL;
	}

	/* Search strategy is glob-only */
	const size_t len = strlen(*arg);

	char *q = savestring(query, len - (invert == 1 ? 2 : 1));
	*arg = xnrealloc(*arg, (len + 3), sizeof(char));

	snprintf((*arg) + 1, len + 2, "*%s*", q);
	free(q);

	return (*arg) + 1;
}

static int
calculate_glob_output_columns(const size_t flongest, const int found)
{
	int columns_n = 0;

	unsigned short tcols = DEFAULT_WIN_COLS;
	struct winsize w;
	if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) != -1 && w.ws_col > 0)
		tcols = w.ws_col;

	if (flongest == 0 || flongest > tcols)
		columns_n = 1;
	else
		columns_n = (int)(tcols / (flongest + 1));

	if (columns_n > found)
		columns_n = found;

	return columns_n;
}

static int
print_glob_matches(struct search_t *matches, const char *search_path)
{
	int found = 0;
	for (found = 0; matches && matches[found].name; found++);
	if (found == 0)
		return 0;

	int eln_pad = search_path ? -1 : 0;
	int longest_eln = -1;
	size_t flongest = 0;

	get_glob_longest(matches, &longest_eln, &flongest, &eln_pad);

	int columns_n = calculate_glob_output_columns(flongest, found);

	/* colors_list() makes use of TAB_OFFSET. We don't want it here. */
	const size_t tab_offset_bk = tab_offset;
	tab_offset = 0;

	for (int i = 0; matches[i].name; i++) {
		const int last_column = (i + 1) % columns_n == 0;

		if (!search_path) {
			/* Print ELN, file indicator, and icon. */
			int index = matches[i].eln - 1;
			char ind_chr = file_info[index].sel == 1 ? SELFILE_CHR : ' ';
			const char *ind_chr_color = file_info[index].sel == 1 ? li_cb : "";

			printf("%s%*d%s%s%c%s%s%s%s%c", el_c, eln_pad, matches[i].eln, df_c,
				ind_chr_color, ind_chr, df_c,
				conf.icons == 1 ? file_info[index].icon_color : "",
				conf.icons == 1 ? file_info[index].icon : "",
				df_c, conf.icons == 1 ? ' ' : 0);
		}

		/* Print filename. */
		int name_pad = (last_column == 1 || i == (found - 1)) ? NO_PAD :
		    (int)(flongest - matches[i].len - (search_path ? 0
		    : (size_t)(eln_pad - DIGINUM(matches[i].eln))) + 1);

		if (name_pad < 0)
			name_pad = 0;

		colors_list(matches[i].name, NO_ELN, name_pad,
		    (last_column == 1 || i == found - 1) ? 1 : NO_NEWLINE);
	}

	tab_offset = tab_offset_bk;

	print_reload_msg(SET_SUCCESS_PTR, xs_cb, _("Matches found: %d%s\n"), found,
		conf.search_strategy != GLOB_ONLY ? " (glob)" : "");

	return found;
}

/* List matching filenames in the specified directory. */
static int
search_glob(char **args)
{
	if (!args || !args[0])
		return FUNC_FAILURE;

	int invert = (args[0][1] == '!');

	char *search_query = NULL, *search_path = NULL;
	mode_t file_type = 0;

	if (set_file_type_and_search_path(args, &file_type,
	&search_path, invert) == FUNC_FAILURE) {
		return ERR_SKIP_REGEX;
	}

	if (file_type == 'x') /* Recursive search via find(1) */
		return FUNC_SUCCESS;

	/* If we have a path ("/str /path"), chdir into it, since glob(3)
	 * works on CWD. */
	if (search_path && *search_path
	&& chdir_search_path(&search_path, args[1]) == FUNC_FAILURE)
		return ERR_SKIP_REGEX;

	search_query = construct_glob_query(&args[0], invert);
	if (!search_query && conf.search_strategy != GLOB_ONLY)
		return FUNC_FAILURE;

	/* Get matches, if any. */
	glob_t globbed_files;
	int ret = glob(search_query, GLOB_BRACE, NULL, &globbed_files);
	if (ret != 0) {
		globfree(&globbed_files);

		/* Go back to the directory we came from */
		if (search_path && xchdir(workspaces[cur_ws].path, NO_TITLE) == -1)
			xerror("search: '%s': %s\n", workspaces[cur_ws].path, strerror(errno));

		return FUNC_FAILURE;
	}

	/* We have matches */

	char **gfiles = globbed_files.gl_pathv;
	size_t g = globbed_files.gl_pathc;

	/* glob(3) doesn't sort directories first. Let's do it ourselves */
	if (conf.list_dirs_first == 1)
		gfiles = glob_sort_dirs(&globbed_files, &g);

	/* We need to store pointers to matching filenames in array of pointers,
	 * just as the filename length (to construct the columned output), and,
	 * if searching in CWD, its index (ELN) in the dirlist array as well. */
	struct search_t *list = NULL;

	if (invert == 0)
		list = get_glob_matches(gfiles, search_path, file_type, g);
	else
		list = get_glob_matches_invert(gfiles, search_path, file_type);

	globfree(&globbed_files);

	int matches = print_glob_matches(list, search_path);

	/* Free stuff */
	if (list) {
		for (size_t i = 0; list[i].name; i++)
			free(list[i].name);
		free(list);
	}

	if (conf.list_dirs_first == 1)
		free(gfiles);

	/* If needed, go back to the directory we came from */
	if (search_path && xchdir(workspaces[cur_ws].path, NO_TITLE) == -1) {
		xerror("search: '%s': %s\n", workspaces[cur_ws].path, strerror(errno));
		return FUNC_FAILURE;
	}

	return (matches == 0 ? FUNC_FAILURE : FUNC_SUCCESS);
}

/* Original string is either "/QUERY" or "/!QUERY". Let's extract QUERY.
 * If the query string contains no metacharacters, change it to ".*QUERY.*" */
static char *
construct_regex_query(char **arg, const int invert, int *regex_found)
{
	char *query = *arg + (invert == 1 ? 2 : 1);

	*regex_found = check_regex(query);
	if (*regex_found == FUNC_SUCCESS)
		return query;

	const size_t len = strlen(*arg);

	char *q = savestring(query, len - (invert == 1 ? 2 : 1));
	*arg = xnrealloc(*arg, (len + 5), sizeof(char));

	snprintf(*arg + 1, len + 4, ".*%s.*", q);
	free(q);

	return *arg + 1;
}

static void
err_regex_no_match(const int regex_found, const char *arg)
{
	char *input = (conf.autocd == 1 && !arg && (regex_found == FUNC_FAILURE
			|| (search_flags & NO_GLOB_CHAR)) && rl_line_buffer)
			? strrchr(rl_line_buffer, '/') : NULL;

	if (input && input != rl_line_buffer) {
		/* Input string contains at least two slashes. It looks like a path:
		 * let's err like it was. */
		char *p = unescape_str(rl_line_buffer);
		xerror("cd: '%s': %s\n", p ? p : rl_line_buffer, strerror(ENOENT));
		free(p);
	} else if (search_flags & NO_GLOB_CHAR) {
		fputs(_("search: No matches found\n"), stderr);
	} else {
		fputs(_("No matches found\n"), stderr);
	}

	search_flags &= ~NO_GLOB_CHAR;
}

static void
free_regex_dirlist(struct dirent ***dirlist, const int tmp_files)
{
	if (!dirlist)
		return;

	int i = tmp_files;
	while (--i >= 0)
		free((*dirlist)[i]);

	free(*dirlist);

	if (xchdir(workspaces[cur_ws].path, NO_TITLE) == -1)
		xerror("search: '%s': %s\n", workspaces[cur_ws].path, strerror(errno));
}

static int
check_regex_file_type(struct dirent **reg_dirlist, const int index,
	const mode_t file_type)
{
	if (reg_dirlist) { /* A search path has been provided. */
#if !defined(_DIRENT_HAVE_D_TYPE)
		mode_t type;
		struct stat attr;
		if (lstat(reg_dirlist[index]->d_name, &attr) == -1)
			return FUNC_FAILURE;

		switch (attr.st_mode & S_IFMT) {
		case S_IFBLK: type = DT_BLK; break;
		case S_IFCHR: type = DT_CHR; break;
		case S_IFDIR: type = DT_DIR; break;
# ifdef SOLARIS_DOORS
		case S_IFDOOR: type = DT_DOOR; break;
		case S_IFPORT: type = DT_PORT; break;
# endif /* SOLARIS_DOORS */
		case S_IFIFO: type = DT_FIFO; break;
		case S_IFLNK: type = DT_LNK; break;
		case S_IFREG: type = DT_REG; break;
		case S_IFSOCK: type = DT_SOCK; break;
		default: type = DT_UNKNOWN; break;
		}

		if (type != file_type)
#else
		if (reg_dirlist[index]->d_type != file_type)
#endif /* !_DIRENT_HAVE_D_TYPE */
			return FUNC_FAILURE;
	} else { /* Searching in CWD. */
		if (file_info[index].type != file_type)
			return FUNC_FAILURE;
	}

	return FUNC_SUCCESS;
}

static struct search_t
load_entry_info(struct dirent **reg_dirlist, const int index)
{
	char *name = reg_dirlist ? reg_dirlist[index]->d_name
		: file_info[index].name;

	struct search_t list = {0};
	list.name = name;
	list.eln = reg_dirlist ? -1 : index + 1;
	list.len = wc_xstrlen(name);
	list.len += (!reg_dirlist && conf.icons == 1) ? (size_t)ICON_LEN : 0;
	list.len += reg_dirlist ? 0 : (size_t)(DIGINUM(list.eln) + 1);

	return list;
}

/* Return the length of the longest entry in the file list LIST. */
static size_t
get_regex_longest(struct search_t *list, const int total, int *elnpad)
{
	int i = total;
	size_t longest_file_len = 0;
	int longest_file_eln = -1;
	int longest_eln = -1;

	while (--i >= 0) {
		if (list[i].len > longest_file_len) {
			longest_file_len = list[i].len;
			longest_file_eln = list[i].eln;
		}
		if (list[i].eln != -1 && list[i].eln > longest_eln)
			longest_eln = list[i].eln;
	}

	*elnpad = DIGINUM(longest_eln);
	longest_file_len += (size_t)(*elnpad - DIGINUM(longest_file_eln));

	return longest_file_len;
}

static size_t
calc_columns(const size_t longest_file_len, const size_t matches)
{
	struct winsize w;
	unsigned short termcols = DEFAULT_WIN_COLS;
	if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) != -1 && w.ws_col > 0)
		termcols = w.ws_col;

	size_t columns;
	if (longest_file_len == 0 || longest_file_len > termcols)
		columns = 1;
	else
		columns = (size_t)termcols / (longest_file_len + 1);

	if (columns > matches)
		columns = matches;

	return columns;
}

static void
print_regex_entry(struct search_t list, const int namepad, const int elnpad,
	const int newline)
{
	if (list.eln != -1) {
		/* Print ELN, file indicator, and icon. */
		int index = list.eln - 1;
		char ind_chr = file_info[index].sel == 1 ? SELFILE_CHR : ' ';
		const char *ind_chr_color = file_info[index].sel == 1 ? li_cb : "";

		printf("%s%*d%s%s%c%s%s%s%s%c", el_c, elnpad,
			list.eln, df_c, ind_chr_color, ind_chr, df_c,
			conf.icons == 1 ? file_info[index].icon_color : "",
			conf.icons == 1 ? file_info[index].icon : "",
			df_c, conf.icons == 1 ? ' ' : 0);
	}

	colors_list(list.name, NO_ELN, namepad, newline);
}

static size_t
print_regex_matches(const mode_t file_type, struct dirent **reg_dirlist,
	const int *regex_index)
{
	/* colors_list() makes use of TAB_OFFSET. We don't need it here. */
	const size_t tab_offset_bk = tab_offset;
	tab_offset = 0;

	size_t total; /* Total number of matches (without file type filter) */
	for (total = 0; regex_index[total] > -1; total++);

	struct search_t *list = xnmalloc(total + 1, sizeof(struct search_t));

	size_t matches = 0; /* Number of filtered matches */

	for (size_t i = total; i-- > 0;) {
		int index = regex_index[i];

		if (file_type != 0 && check_regex_file_type(reg_dirlist,
		index, file_type) == FUNC_FAILURE)
			continue;

		list[matches++] = load_entry_info(reg_dirlist, index);
	}

	size_t count = 0;
	if (matches == 0) {
		fputs(_("search: No matches found\n"), stderr);
		goto END;
	}

	int eln_pad = -1;
	const size_t longest_len = get_regex_longest(list, (int)matches, &eln_pad);
	const size_t columns = calc_columns(longest_len, matches);

	size_t cur_col = 0;

	for (size_t i = matches; i-- > 0;) {
		cur_col++;
		count++;

		size_t last_col = 0;
		if (cur_col == columns) {
			last_col = 1;
			cur_col = 0;
		}

		/* Calculate how much right pad we need for the current entry */
		int name_pad = (last_col == 1 || count == matches) ? NO_PAD :
			(int)(longest_len - list[i].len - (list[i].eln == -1 ? 0
			: (size_t)(eln_pad - DIGINUM(list[i].eln))) + 1);

		if (name_pad < 0)
			name_pad = 0;

		int newline = (last_col == 1 || count == matches);
		print_regex_entry(list[i], name_pad, eln_pad, newline);
	}

	print_reload_msg(SET_SUCCESS_PTR, xs_cb, _("Matches found: %zu\n"), count);

END:
	free(list);
	tab_offset = tab_offset_bk;
	return count;
}

/* List matching (or non-matching if INVERT is set to 1) filenames
 * in the specified directory. */
static int
search_regex(char **args)
{
	if (!args || !args[0])
		return FUNC_FAILURE;

	int invert = (args[0][1] == '!');
	char *search_query = NULL, *search_path = NULL;
	mode_t file_type = 0;

	if (set_file_type_and_search_path(args, &file_type,
	&search_path, 1) == FUNC_FAILURE)
		return FUNC_FAILURE;

	if (file_type == 'x') /* Recursive search via find(1) */
		return FUNC_SUCCESS;

	struct dirent **reg_dirlist = NULL;
	int tmp_files = -1;

	if (search_path && *search_path) {
		if (chdir_search_path(&search_path, args[1]) == FUNC_FAILURE)
			return FUNC_FAILURE;

		tmp_files = scandir(".", &reg_dirlist, skip_files, xalphasort);
		if (tmp_files == -1) {
			xerror("search: '%s': %s\n", search_path, strerror(errno));

			if (xchdir(workspaces[cur_ws].path, NO_TITLE) == -1)
				xerror("search: '%s': %s\n", workspaces[cur_ws].path,
					strerror(errno));

			return FUNC_FAILURE;
		}
	}

	int regex_found = 0;
	search_query = construct_regex_query(&args[0], invert, &regex_found);

	/* Get matches */
	regex_t regex_files;
	int reg_flags = conf.case_sens_search == 1 ? (REG_NOSUB | REG_EXTENDED)
		: (REG_NOSUB | REG_EXTENDED | REG_ICASE);
	int ret = regcomp(&regex_files, search_query, reg_flags);

	if (ret != FUNC_SUCCESS) {
		xerror(_("'%s': Invalid regular expression\n"), search_query);
		regfree(&regex_files);
		free_regex_dirlist(&reg_dirlist, tmp_files);
		return FUNC_FAILURE;
	}

	size_t found = 0;
	int *regex_index = xnmalloc((search_path ? (size_t)tmp_files
		: (size_t)g_files_num) + 2, sizeof(int));
	const size_t max =
		(search_path && *search_path) ? (size_t)tmp_files : (size_t)g_files_num;

	for (size_t i = 0; i < max; i++) {
		char *name = (search_path && *search_path) ? reg_dirlist[i]->d_name
		: file_info[i].name;

		if (regexec(&regex_files, name, 0, NULL, 0) == FUNC_SUCCESS) {
			if (invert == 0)
				regex_index[found++] = (int)i;
		} else {
			if (invert == 1)
				regex_index[found++] = (int)i;
		}
	}

	regex_index[found] = -1; /* Mark end of array */
	regfree(&regex_files);

	if (found == 0) {
		err_regex_no_match(regex_found, args[1]);
		free(regex_index);
		free_regex_dirlist(&reg_dirlist, tmp_files);
		return FUNC_FAILURE;
	}

	/* We have matches: print them. */
	const size_t matches =
		print_regex_matches(file_type, reg_dirlist, regex_index);

	free(regex_index);
	free_regex_dirlist(&reg_dirlist, tmp_files);

	return matches == 0 ? FUNC_FAILURE : FUNC_SUCCESS;
}

static int
err_glob_no_match(const char *arg)
{
	char *input = (conf.autocd == 1 && !arg
		&& (search_flags & NO_GLOB_CHAR)
		&& rl_line_buffer) ? strrchr(rl_line_buffer, '/') : NULL;

	if (input && input != rl_line_buffer) {
		/* Input string contains two slashes: it looks like a path, so let's
		 * err like it was. */
		char *p = unescape_str(rl_line_buffer);
		xerror("cd: '%s': %s\n", p ? p : rl_line_buffer, strerror(ENOENT));
		free(p);
		return FUNC_FAILURE;
	}

	fputs(_("search: No matches found\n"), stderr);
	return FUNC_FAILURE;
}

/* We provide three search strategies:
 * 1 - Only glob
 * 2 - Only regex
 * 3 - Glob-regex */
int
search_function(char **args)
{
	if (args[1] && IS_HELP(args[1])) {
		puts(SEARCH_USAGE);
		return FUNC_SUCCESS;
	}

	if (conf.search_strategy == REGEX_ONLY)
		return search_regex(args);

	const int ret = search_glob(args);
	if (ret != FUNC_FAILURE)
		return (ret == ERR_SKIP_REGEX ? 1 : ret);

	if (conf.search_strategy == GLOB_ONLY)
		return err_glob_no_match(args[1]);

	if (!(search_flags & NO_GLOB_CHAR))
		fputs(_("Glob: No matches found. Trying regex...\n"), stderr);

	return search_regex(args);
}
