vis

a vi-like editor based on Plan 9's structural regular expressions

git clone https://9o.is/git/vis.git

vis-cmds.c

(26887B)


      1 /* this file is included from sam.c */
      2 
      3 #include <termkey.h>
      4 #include "vis-lua.h"
      5 
      6 // FIXME: avoid this redirection?
      7 typedef struct {
      8 	CommandDef def;
      9 	VisCommandFunction *func;
     10 	void *data;
     11 } CmdUser;
     12 
     13 static void cmdfree(CmdUser *cmd) {
     14 	if (!cmd)
     15 		return;
     16 	free((char*)cmd->def.name);
     17 	free(VIS_HELP_USE((char*)cmd->def.help));
     18 	free(cmd);
     19 }
     20 
     21 bool vis_cmd_register(Vis *vis, const char *name, const char *help, void *data, VisCommandFunction *func) {
     22 	if (!name)
     23 		return false;
     24 	if (!vis->usercmds && !(vis->usercmds = map_new()))
     25 		return false;
     26 	CmdUser *cmd = calloc(1, sizeof *cmd);
     27 	if (!cmd)
     28 		return false;
     29 	if (!(cmd->def.name = strdup(name)))
     30 		goto err;
     31 #if CONFIG_HELP
     32 	if (help && !(cmd->def.help = strdup(help)))
     33 		goto err;
     34 #endif
     35 	cmd->def.flags = CMD_ARGV|CMD_FORCE|CMD_ONCE|CMD_ADDRESS_ALL;
     36 	cmd->def.func = cmd_user;
     37 	cmd->func = func;
     38 	cmd->data = data;
     39 	if (!map_put(vis->cmds, name, &cmd->def))
     40 		goto err;
     41 	if (!map_put(vis->usercmds, name, cmd)) {
     42 		map_delete(vis->cmds, name);
     43 		goto err;
     44 	}
     45 	return true;
     46 err:
     47 	cmdfree(cmd);
     48 	return false;
     49 }
     50 
     51 bool vis_cmd_unregister(Vis *vis, const char *name) {
     52 	if (!name)
     53 		return true;
     54 	CmdUser *cmd = map_get(vis->usercmds, name);
     55 	if (!cmd)
     56 		return false;
     57 	if (!map_delete(vis->cmds, name))
     58 		return false;
     59 	if (!map_delete(vis->usercmds, name))
     60 		return false;
     61 	cmdfree(cmd);
     62 	return true;
     63 }
     64 
     65 static void option_free(OptionDef *opt) {
     66 	if (!opt)
     67 		return;
     68 	for (size_t i = 0; i < LENGTH(options); i++) {
     69 		if (opt == &options[i])
     70 			return;
     71 	}
     72 
     73 	for (const char **name = opt->names; *name; name++)
     74 		free((char*)*name);
     75 	free(VIS_HELP_USE((char*)opt->help));
     76 	free(opt);
     77 }
     78 
     79 bool vis_option_register(Vis *vis, const char *names[], enum VisOption flags,
     80                          VisOptionFunction *func, void *context, const char *help) {
     81 
     82 	if (!names || !names[0])
     83 		return false;
     84 
     85 	for (const char **name = names; *name; name++) {
     86 		if (map_get(vis->options, *name))
     87 			return false;
     88 	}
     89 	OptionDef *opt = calloc(1, sizeof *opt);
     90 	if (!opt)
     91 		return false;
     92 	for (size_t i = 0; i < LENGTH(opt->names)-1 && names[i]; i++) {
     93 		if (!(opt->names[i] = strdup(names[i])))
     94 			goto err;
     95 	}
     96 	opt->flags = flags;
     97 	opt->func = func;
     98 	opt->context = context;
     99 #if CONFIG_HELP
    100 	if (help && !(opt->help = strdup(help)))
    101 		goto err;
    102 #endif
    103 	for (const char **name = names; *name; name++)
    104 		map_put(vis->options, *name, opt);
    105 	return true;
    106 err:
    107 	option_free(opt);
    108 	return false;
    109 }
    110 
    111 bool vis_option_unregister(Vis *vis, const char *name) {
    112 	OptionDef *opt = map_get(vis->options, name);
    113 	if (!opt)
    114 		return false;
    115 	for (const char **alias = opt->names; *alias; alias++) {
    116 		if (!map_delete(vis->options, *alias))
    117 			return false;
    118 	}
    119 	option_free(opt);
    120 	return true;
    121 }
    122 
    123 static bool cmd_user(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
    124 	CmdUser *user = map_get(vis->usercmds, argv[0]);
    125 	return user && user->func(vis, win, user->data, cmd->flags == '!', argv, sel, range);
    126 }
    127 
    128 void vis_shell_set(Vis *vis, const char *new_shell) {
    129 	char *shell =  strdup(new_shell);
    130 	if (!shell) {
    131 		vis_info_show(vis, "Failed to change shell");
    132 	} else {
    133 		free(vis->shell);
    134 		vis->shell = shell;
    135 	}
    136 }
    137 
    138 /* parse human-readable boolean value in s. If successful, store the result in
    139  * outval and return true. Else return false and leave outval alone. */
    140 static bool parse_bool(const char *s, bool *outval) {
    141 	for (const char **t = (const char*[]){"1", "true", "yes", "on", NULL}; *t; t++) {
    142 		if (!strcasecmp(s, *t)) {
    143 			*outval = true;
    144 			return true;
    145 		}
    146 	}
    147 	for (const char **f = (const char*[]){"0", "false", "no", "off", NULL}; *f; f++) {
    148 		if (!strcasecmp(s, *f)) {
    149 			*outval = false;
    150 			return true;
    151 		}
    152 	}
    153 	return false;
    154 }
    155 
    156 static bool cmd_set(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
    157 
    158 	if (!argv[1] || !argv[1][0] || argv[3]) {
    159 		vis_info_show(vis, "Expecting: set option [value]");
    160 		return false;
    161 	}
    162 
    163 	char name[256];
    164 	strncpy(name, argv[1], sizeof(name)-1);
    165 	char *lastchar = &name[strlen(name)-1];
    166 	bool toggle = (*lastchar == '!');
    167 	if (toggle)
    168 		*lastchar = '\0';
    169 
    170 	OptionDef *opt = map_closest(vis->options, name);
    171 	if (!opt) {
    172 		vis_info_show(vis, "Unknown option: `%s'", name);
    173 		return false;
    174 	}
    175 
    176 	if (opt->flags & VIS_OPTION_DEPRECATED && strcmp(opt->context, name) == 0)
    177 		vis_info_show(vis, "%s is deprecated and will be removed in the next release", name);
    178 
    179 	if (!win && (opt->flags & VIS_OPTION_NEED_WINDOW)) {
    180 		vis_info_show(vis, "Need active window for `:set %s'", name);
    181 		return false;
    182 	}
    183 
    184 	if (toggle) {
    185 		if (!(opt->flags & VIS_OPTION_TYPE_BOOL)) {
    186 			vis_info_show(vis, "Only boolean options can be toggled");
    187 			return false;
    188 		}
    189 		if (argv[2]) {
    190 			vis_info_show(vis, "Can not specify option value when toggling");
    191 			return false;
    192 		}
    193 	}
    194 
    195 	Arg arg;
    196 	if (opt->flags & VIS_OPTION_TYPE_STRING) {
    197 		if (!(opt->flags & VIS_OPTION_VALUE_OPTIONAL) && !argv[2]) {
    198 			vis_info_show(vis, "Expecting string option value");
    199 			return false;
    200 		}
    201 		arg.s = argv[2];
    202 	} else if (opt->flags & VIS_OPTION_TYPE_BOOL) {
    203 		if (!argv[2]) {
    204 			arg.b = !toggle;
    205 		} else if (!parse_bool(argv[2], &arg.b)) {
    206 			vis_info_show(vis, "Expecting boolean option value not: `%s'", argv[2]);
    207 			return false;
    208 		}
    209 	} else if (opt->flags & VIS_OPTION_TYPE_NUMBER) {
    210 		if (!argv[2]) {
    211 			vis_info_show(vis, "Expecting number");
    212 			return false;
    213 		}
    214 		char *ep;
    215 		errno = 0;
    216 		long lval = strtol(argv[2], &ep, 10);
    217 		if (argv[2][0] == '\0' || *ep != '\0') {
    218 			vis_info_show(vis, "Invalid number");
    219 			return false;
    220 		}
    221 
    222 		if ((errno == ERANGE && (lval == LONG_MAX || lval == LONG_MIN)) ||
    223 		    (lval > INT_MAX || lval < INT_MIN)) {
    224 			vis_info_show(vis, "Number overflow");
    225 			return false;
    226 		}
    227 
    228 		if (lval < 0) {
    229 			vis_info_show(vis, "Expecting positive number");
    230 			return false;
    231 		}
    232 		arg.i = lval;
    233 	} else {
    234 		return false;
    235 	}
    236 
    237 	size_t opt_index = 0;
    238 	for (; opt_index < LENGTH(options); opt_index++) {
    239 		if (opt == &options[opt_index])
    240 			break;
    241 	}
    242 
    243 	switch (opt_index) {
    244 	case OPTION_SHELL:
    245 		vis_shell_set(vis, arg.s);
    246 		break;
    247 	case OPTION_ESCDELAY:
    248 	{
    249 		termkey_set_waittime(vis->ui.termkey, arg.i);
    250 		break;
    251 	}
    252 	case OPTION_EXPANDTAB:
    253 		vis->win->expandtab = toggle ? !vis->win->expandtab : arg.b;
    254 		break;
    255 	case OPTION_AUTOINDENT:
    256 		vis->autoindent = toggle ? !vis->autoindent : arg.b;
    257 		break;
    258 	case OPTION_TABWIDTH:
    259 		view_tabwidth_set(&vis->win->view, arg.i);
    260 		break;
    261 	case OPTION_SHOW_SPACES:
    262 	case OPTION_SHOW_TABS:
    263 	case OPTION_SHOW_NEWLINES:
    264 	case OPTION_SHOW_EOF:
    265 	case OPTION_STATUSBAR:
    266 	{
    267 		const int values[] = {
    268 			[OPTION_SHOW_SPACES] = UI_OPTION_SYMBOL_SPACE,
    269 			[OPTION_SHOW_TABS] = UI_OPTION_SYMBOL_TAB|UI_OPTION_SYMBOL_TAB_FILL,
    270 			[OPTION_SHOW_NEWLINES] = UI_OPTION_SYMBOL_EOL,
    271 			[OPTION_SHOW_EOF] = UI_OPTION_SYMBOL_EOF,
    272 			[OPTION_STATUSBAR] = UI_OPTION_STATUSBAR,
    273 		};
    274 		int flags = win->options;
    275 		if (arg.b || (toggle && !(flags & values[opt_index])))
    276 			flags |= values[opt_index];
    277 		else
    278 			flags &= ~values[opt_index];
    279 		win_options_set(win, flags);
    280 		break;
    281 	}
    282 	case OPTION_NUMBER: {
    283 		enum UiOption opt = win->options;
    284 		if (arg.b || (toggle && !(opt & UI_OPTION_LINE_NUMBERS_ABSOLUTE))) {
    285 			opt &= ~UI_OPTION_LINE_NUMBERS_RELATIVE;
    286 			opt |=  UI_OPTION_LINE_NUMBERS_ABSOLUTE;
    287 		} else {
    288 			opt &= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE;
    289 		}
    290 		win_options_set(win, opt);
    291 		break;
    292 	}
    293 	case OPTION_NUMBER_RELATIVE: {
    294 		enum UiOption opt = win->options;
    295 		if (arg.b || (toggle && !(opt & UI_OPTION_LINE_NUMBERS_RELATIVE))) {
    296 			opt &= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE;
    297 			opt |=  UI_OPTION_LINE_NUMBERS_RELATIVE;
    298 		} else {
    299 			opt &= ~UI_OPTION_LINE_NUMBERS_RELATIVE;
    300 		}
    301 		win_options_set(win, opt);
    302 		break;
    303 	}
    304 	case OPTION_CURSOR_LINE: {
    305 		enum UiOption opt = win->options;
    306 		if (arg.b || (toggle && !(opt & UI_OPTION_CURSOR_LINE)))
    307 			opt |= UI_OPTION_CURSOR_LINE;
    308 		else
    309 			opt &= ~UI_OPTION_CURSOR_LINE;
    310 		win_options_set(win, opt);
    311 		break;
    312 	}
    313 	case OPTION_COLOR_COLUMN:
    314 		if (arg.i >= 0)
    315 			win->view.colorcolumn = arg.i;
    316 		break;
    317 	case OPTION_SAVE_METHOD:
    318 		if (strcmp("auto", arg.s) == 0) {
    319 			win->file->save_method = TEXT_SAVE_AUTO;
    320 		} else if (strcmp("atomic", arg.s) == 0) {
    321 			win->file->save_method = TEXT_SAVE_ATOMIC;
    322 		} else if (strcmp("inplace", arg.s) == 0) {
    323 			win->file->save_method = TEXT_SAVE_INPLACE;
    324 		} else {
    325 			vis_info_show(vis, "Invalid save method `%s', expected "
    326 			              "'auto', 'atomic' or 'inplace'", arg.s);
    327 			return false;
    328 		}
    329 		break;
    330 	case OPTION_LOAD_METHOD:
    331 		if (strcmp("auto", arg.s) == 0) {
    332 			vis->load_method = TEXT_LOAD_AUTO;
    333 		} else if (strcmp("read", arg.s) == 0) {
    334 			vis->load_method = TEXT_LOAD_READ;
    335 		} else if (strcmp("mmap", arg.s) == 0) {
    336 			vis->load_method = TEXT_LOAD_MMAP;
    337 		} else {
    338 			vis_info_show(vis, "Invalid load method `%s', expected "
    339 			              "'auto', 'read' or 'mmap'", arg.s);
    340 			return false;
    341 		}
    342 		break;
    343 	case OPTION_CHANGE_256COLORS:
    344 		vis->change_colors = toggle ? !vis->change_colors : arg.b;
    345 		break;
    346 	case OPTION_LAYOUT: {
    347 		enum UiLayout layout;
    348 		if (strcmp("h", arg.s) == 0) {
    349 			layout = UI_LAYOUT_HORIZONTAL;
    350 		} else if (strcmp("v", arg.s) == 0) {
    351 			layout = UI_LAYOUT_VERTICAL;
    352 		} else {
    353 			vis_info_show(vis, "Invalid layout `%s', expected 'h' or 'v'", arg.s);
    354 			return false;
    355 		}
    356 		ui_arrange(&vis->ui, layout);
    357 		break;
    358 	}
    359 	case OPTION_IGNORECASE:
    360 		vis->ignorecase = toggle ? !vis->ignorecase : arg.b;
    361 		break;
    362 	case OPTION_BREAKAT:
    363 		if (!view_breakat_set(&win->view, arg.s)) {
    364 			vis_info_show(vis, "Failed to set breakat");
    365 			return false;
    366 		}
    367 		break;
    368 	case OPTION_WRAP_COLUMN:
    369 		if (arg.i >= 0)
    370 			win->view.wrapcolumn = arg.i;
    371 		break;
    372 	default:
    373 		if (!opt->func)
    374 			return false;
    375 		return opt->func(vis, win, opt->context, toggle, opt->flags, name, &arg);
    376 	}
    377 
    378 	return true;
    379 }
    380 
    381 static bool is_file_pattern(const char *pattern) {
    382 	if (!pattern)
    383 		return false;
    384 	struct stat meta;
    385 	if (stat(pattern, &meta) == 0 && S_ISDIR(meta.st_mode))
    386 		return true;
    387 	/* tilde expansion is defined only for the tilde at the
    388 	   beginning of the pattern. */
    389 	if (pattern[0] == '~')
    390 		return true;
    391 	for (char special[] = "*?[{$", *s = special; *s; s++) {
    392 		if (strchr(pattern, *s))
    393 			return true;
    394 	}
    395 	return false;
    396 }
    397 
    398 static const char *file_open_dialog(Vis *vis, const char *pattern) {
    399 	static char name[PATH_MAX];
    400 	name[0] = '\0';
    401 
    402 	if (!is_file_pattern(pattern))
    403 		return pattern;
    404 
    405 	Buffer bufcmd = {0}, bufout = {0}, buferr = {0};
    406 
    407 	if (!buffer_put0(&bufcmd, VIS_OPEN " ") || !buffer_append0(&bufcmd, pattern ? pattern : ""))
    408 		return NULL;
    409 
    410 	Filerange empty = text_range_new(0,0);
    411 	int status = vis_pipe(vis, vis->win->file, &empty,
    412 		(const char*[]){ buffer_content0(&bufcmd), NULL },
    413 		&bufout, read_into_buffer, &buferr, read_into_buffer, false);
    414 
    415 	if (status == 0)
    416 		strncpy(name, buffer_content0(&bufout), sizeof(name)-1);
    417 	else if (status != 1)
    418 		vis_info_show(vis, "Command failed %s", buffer_content0(&buferr));
    419 
    420 	buffer_release(&bufcmd);
    421 	buffer_release(&bufout);
    422 	buffer_release(&buferr);
    423 
    424 	for (char *end = name+strlen(name)-1; end >= name && isspace((unsigned char)*end); end--)
    425 		*end = '\0';
    426 
    427 	return name[0] ? name : NULL;
    428 }
    429 
    430 static bool openfiles(Vis *vis, const char **files) {
    431 	for (; *files; files++) {
    432 		const char *file = file_open_dialog(vis, *files);
    433 		if (!file)
    434 			return false;
    435 		errno = 0;
    436 		if (!vis_window_new(vis, file)) {
    437 			vis_info_show(vis, "Could not open `%s' %s", file,
    438 			                 errno ? strerror(errno) : "");
    439 			return false;
    440 		}
    441 	}
    442 	return true;
    443 }
    444 
    445 static bool cmd_open(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
    446 	if (!argv[1])
    447 		return vis_window_new(vis, NULL);
    448 	return openfiles(vis, &argv[1]);
    449 }
    450 
    451 static void info_unsaved_changes(Vis *vis) {
    452 	vis_info_show(vis, "No write since last change (add ! to override)");
    453 }
    454 
    455 static bool cmd_edit(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
    456 	if (argv[2]) {
    457 		vis_info_show(vis, "Only 1 filename allowed");
    458 		return false;
    459 	}
    460 	Win *oldwin = win;
    461 	if (!oldwin)
    462 		return false;
    463 	if (cmd->flags != '!' && !vis_window_closable(oldwin)) {
    464 		info_unsaved_changes(vis);
    465 		return false;
    466 	}
    467 	if (!argv[1]) {
    468 		if (oldwin->file->refcount > 1) {
    469 			vis_info_show(vis, "Can not reload file being opened multiple times");
    470 			return false;
    471 		}
    472 		return vis_window_reload(oldwin);
    473 	}
    474 	if (!openfiles(vis, &argv[1]))
    475 		return false;
    476 	if (vis->win != oldwin) {
    477 		Win *newwin = vis->win;
    478 		vis_window_swap(oldwin, newwin);
    479 		vis_window_close(oldwin);
    480 		vis_window_focus(newwin);
    481 	}
    482 	return vis->win != oldwin;
    483 }
    484 
    485 static bool cmd_read(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
    486 	bool ret = false;
    487 	const size_t first_file = 3;
    488 	const char *args[MAX_ARGV] = { argv[0], "cat", "--" };
    489 	const char **name = argv[1] ? &argv[1] : (const char*[]){ ".", NULL };
    490 	for (size_t i = first_file; *name && i < LENGTH(args)-1; name++, i++) {
    491 		const char *file = file_open_dialog(vis, *name);
    492 		if (!file || !(args[i] = strdup(file)))
    493 			goto err;
    494 	}
    495 	args[LENGTH(args)-1] = NULL;
    496 	ret = cmd_pipein(vis, win, cmd, args, sel, range);
    497 err:
    498 	for (size_t i = first_file; i < LENGTH(args); i++)
    499 		free((char*)args[i]);
    500 	return ret;
    501 }
    502 
    503 static bool has_windows(Vis *vis) {
    504 	for (Win *win = vis->windows; win; win = win->next) {
    505 		if (!win->file->internal)
    506 			return true;
    507 	}
    508 	return false;
    509 }
    510 
    511 static bool cmd_quit(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
    512 	if (cmd->flags != '!' && !vis_window_closable(win)) {
    513 		info_unsaved_changes(vis);
    514 		return false;
    515 	}
    516 	vis_window_close(win);
    517 	if (!has_windows(vis))
    518 		vis_exit(vis, argv[1] ? atoi(argv[1]) : EXIT_SUCCESS);
    519 	return true;
    520 }
    521 
    522 static bool cmd_qall(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
    523 	for (Win *next, *win = vis->windows; win; win = next) {
    524 		next = win->next;
    525 		if (!win->file->internal && (!text_modified(win->file->text) || cmd->flags == '!'))
    526 			vis_window_close(win);
    527 	}
    528 	if (!has_windows(vis)) {
    529 		vis_exit(vis, argv[1] ? atoi(argv[1]) : EXIT_SUCCESS);
    530 		return true;
    531 	} else {
    532 		info_unsaved_changes(vis);
    533 		return false;
    534 	}
    535 }
    536 
    537 static bool cmd_split(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
    538 	if (!win)
    539 		return false;
    540 	enum UiOption options = win->options;
    541 	ui_arrange(&vis->ui, UI_LAYOUT_HORIZONTAL);
    542 	if (!argv[1])
    543 		return vis_window_split(win);
    544 	bool ret = openfiles(vis, &argv[1]);
    545 	if (ret)
    546 		win_options_set(vis->win, options);
    547 	return ret;
    548 }
    549 
    550 static bool cmd_vsplit(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
    551 	if (!win)
    552 		return false;
    553 	enum UiOption options = win->options;
    554 	ui_arrange(&vis->ui, UI_LAYOUT_VERTICAL);
    555 	if (!argv[1])
    556 		return vis_window_split(win);
    557 	bool ret = openfiles(vis, &argv[1]);
    558 	if (ret)
    559 		win_options_set(vis->win, options);
    560 	return ret;
    561 }
    562 
    563 static bool cmd_new(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
    564 	ui_arrange(&vis->ui, UI_LAYOUT_HORIZONTAL);
    565 	return vis_window_new(vis, NULL);
    566 }
    567 
    568 static bool cmd_vnew(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
    569 	ui_arrange(&vis->ui, UI_LAYOUT_VERTICAL);
    570 	return vis_window_new(vis, NULL);
    571 }
    572 
    573 static bool cmd_wq(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
    574 	if (!win)
    575 		return false;
    576 	File *file = win->file;
    577 	bool unmodified = file->fd == -1 && !file->name && !text_modified(file->text);
    578 	if (unmodified || cmd_write(vis, win, cmd, argv, sel, range))
    579 		return cmd_quit(vis, win, cmd, (const char*[]){argv[0], NULL}, sel, range);
    580 	return false;
    581 }
    582 
    583 static bool cmd_earlier_later(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
    584 	if (!win)
    585 		return false;
    586 	Text *txt = win->file->text;
    587 	char *unit = "";
    588 	long count = 1;
    589 	size_t pos = EPOS;
    590 	if (argv[1]) {
    591 		errno = 0;
    592 		count = strtol(argv[1], &unit, 10);
    593 		if (errno || unit == argv[1] || count < 0) {
    594 			vis_info_show(vis, "Invalid number");
    595 			return false;
    596 		}
    597 
    598 		if (*unit) {
    599 			while (*unit && isspace((unsigned char)*unit))
    600 				unit++;
    601 			switch (*unit) {
    602 			case 'd': count *= 24; /* fall through */
    603 			case 'h': count *= 60; /* fall through */
    604 			case 'm': count *= 60; /* fall through */
    605 			case 's': break;
    606 			default:
    607 				vis_info_show(vis, "Unknown time specifier (use: s,m,h or d)");
    608 				return false;
    609 			}
    610 
    611 			if (argv[0][0] == 'e')
    612 				count = -count; /* earlier, move back in time */
    613 
    614 			pos = text_restore(txt, text_state(txt) + count);
    615 		}
    616 	}
    617 
    618 	if (!*unit) {
    619 		VisCountIterator it = vis_count_iterator_init(vis, count);
    620 		while (vis_count_iterator_next(&it)) {
    621 			if (argv[0][0] == 'e')
    622 				pos = text_earlier(txt);
    623 			else
    624 				pos = text_later(txt);
    625 		}
    626 	}
    627 
    628 	struct tm tm;
    629 	time_t state = text_state(txt);
    630 	char buf[32];
    631 	strftime(buf, sizeof buf, "State from %H:%M", localtime_r(&state, &tm));
    632 	vis_info_show(vis, "%s", buf);
    633 
    634 	return pos != EPOS;
    635 }
    636 
    637 static int space_replace(char *dest, const char *src, size_t dlen) {
    638 	int invisiblebytes = 0;
    639 	size_t i, size = LENGTH("␣") - 1;
    640 	for (i = 0; *src && i < dlen; src++) {
    641 		if (*src == ' ' && i < dlen - size - 1) {
    642 			memcpy(&dest[i], "␣", size);
    643 			i += size;
    644 			invisiblebytes += size - 1;
    645 		} else {
    646 			dest[i] = *src;
    647 			i++;
    648 		}
    649 	}
    650 	dest[i] = '\0';
    651 	return invisiblebytes;
    652 }
    653 
    654 static bool print_keylayout(const char *key, void *value, void *data) {
    655 	char buf[64];
    656 	int invisiblebytes = space_replace(buf, key, sizeof(buf));
    657 	return text_appendf(data, "  %-*s\t%s\n", 18+invisiblebytes, buf, (char*)value);
    658 }
    659 
    660 static bool print_keybinding(const char *key, void *value, void *data) {
    661 	KeyBinding *binding = value;
    662 	const char *desc = binding->alias;
    663 	if (!desc && binding->action)
    664 		desc = VIS_HELP_USE(binding->action->help);
    665 	char buf[64];
    666 	int invisiblebytes = space_replace(buf, key, sizeof(buf));
    667 	return text_appendf(data, "  %-*s\t%s\n", 18+invisiblebytes, buf, desc ? desc : "");
    668 }
    669 
    670 static void print_mode(Mode *mode, Text *txt) {
    671 	if (!map_empty(mode->bindings))
    672 		text_appendf(txt, "\n %s\n\n", mode->name);
    673 	map_iterate(mode->bindings, print_keybinding, txt);
    674 }
    675 
    676 static bool print_action(const char *key, void *value, void *data) {
    677 	const char *help = VIS_HELP_USE(((KeyAction*)value)->help);
    678 	return text_appendf(data, "  %-30s\t%s\n", key, help ? help : "");
    679 }
    680 
    681 static bool print_cmd(const char *key, void *value, void *data) {
    682 	CommandDef *cmd = value;
    683 	const char *help = VIS_HELP_USE(cmd->help);
    684 	char usage[256];
    685 	snprintf(usage, sizeof usage, "%s%s%s%s%s%s%s",
    686 	         cmd->name,
    687 	         (cmd->flags & CMD_FORCE) ? "[!]" : "",
    688 	         (cmd->flags & CMD_TEXT) ? "/text/" : "",
    689 	         (cmd->flags & CMD_REGEX) ? "/regexp/" : "",
    690 	         (cmd->flags & CMD_CMD) ? " command" : "",
    691 	         (cmd->flags & CMD_SHELL) ? (!strcmp(cmd->name, "s") ? "/regexp/text/" : " shell-command") : "",
    692 	         (cmd->flags & CMD_ARGV) ? " [args...]" : "");
    693 	return text_appendf(data, "  %-30s %s\n", usage, help ? help : "");
    694 }
    695 
    696 static bool print_option(const char *key, void *value, void *txt) {
    697 	char desc[256];
    698 	const OptionDef *opt = value;
    699 	const char *help = VIS_HELP_USE(opt->help);
    700 	if (strcmp(key, opt->names[0]))
    701 		return true;
    702 	snprintf(desc, sizeof desc, "%s%s%s%s%s",
    703 	         opt->names[0],
    704 	         opt->names[1] ? "|" : "",
    705 	         opt->names[1] ? opt->names[1] : "",
    706 	         opt->flags & VIS_OPTION_TYPE_BOOL ? " on|off" : "",
    707 	         opt->flags & VIS_OPTION_TYPE_NUMBER ? " nn" : "");
    708 	return text_appendf(txt, "  %-30s %s\n", desc, help ? help : "");
    709 }
    710 
    711 static void print_symbolic_keys(Vis *vis, Text *txt) {
    712 	static const int keys[] = {
    713 		TERMKEY_SYM_BACKSPACE,
    714 		TERMKEY_SYM_TAB,
    715 		TERMKEY_SYM_ENTER,
    716 		TERMKEY_SYM_ESCAPE,
    717 		//TERMKEY_SYM_SPACE,
    718 		TERMKEY_SYM_DEL,
    719 		TERMKEY_SYM_UP,
    720 		TERMKEY_SYM_DOWN,
    721 		TERMKEY_SYM_LEFT,
    722 		TERMKEY_SYM_RIGHT,
    723 		TERMKEY_SYM_BEGIN,
    724 		TERMKEY_SYM_FIND,
    725 		TERMKEY_SYM_INSERT,
    726 		TERMKEY_SYM_DELETE,
    727 		TERMKEY_SYM_SELECT,
    728 		TERMKEY_SYM_PAGEUP,
    729 		TERMKEY_SYM_PAGEDOWN,
    730 		TERMKEY_SYM_HOME,
    731 		TERMKEY_SYM_END,
    732 		TERMKEY_SYM_CANCEL,
    733 		TERMKEY_SYM_CLEAR,
    734 		TERMKEY_SYM_CLOSE,
    735 		TERMKEY_SYM_COMMAND,
    736 		TERMKEY_SYM_COPY,
    737 		TERMKEY_SYM_EXIT,
    738 		TERMKEY_SYM_HELP,
    739 		TERMKEY_SYM_MARK,
    740 		TERMKEY_SYM_MESSAGE,
    741 		TERMKEY_SYM_MOVE,
    742 		TERMKEY_SYM_OPEN,
    743 		TERMKEY_SYM_OPTIONS,
    744 		TERMKEY_SYM_PRINT,
    745 		TERMKEY_SYM_REDO,
    746 		TERMKEY_SYM_REFERENCE,
    747 		TERMKEY_SYM_REFRESH,
    748 		TERMKEY_SYM_REPLACE,
    749 		TERMKEY_SYM_RESTART,
    750 		TERMKEY_SYM_RESUME,
    751 		TERMKEY_SYM_SAVE,
    752 		TERMKEY_SYM_SUSPEND,
    753 		TERMKEY_SYM_UNDO,
    754 		TERMKEY_SYM_KP0,
    755 		TERMKEY_SYM_KP1,
    756 		TERMKEY_SYM_KP2,
    757 		TERMKEY_SYM_KP3,
    758 		TERMKEY_SYM_KP4,
    759 		TERMKEY_SYM_KP5,
    760 		TERMKEY_SYM_KP6,
    761 		TERMKEY_SYM_KP7,
    762 		TERMKEY_SYM_KP8,
    763 		TERMKEY_SYM_KP9,
    764 		TERMKEY_SYM_KPENTER,
    765 		TERMKEY_SYM_KPPLUS,
    766 		TERMKEY_SYM_KPMINUS,
    767 		TERMKEY_SYM_KPMULT,
    768 		TERMKEY_SYM_KPDIV,
    769 		TERMKEY_SYM_KPCOMMA,
    770 		TERMKEY_SYM_KPPERIOD,
    771 		TERMKEY_SYM_KPEQUALS,
    772 	};
    773 
    774 	TermKey *termkey = vis->ui.termkey;
    775 	text_appendf(txt, "  ␣ (a literal \" \" space symbol must be used to refer to <Space>)\n");
    776 	for (size_t i = 0; i < LENGTH(keys); i++) {
    777 		text_appendf(txt, "  <%s>\n", termkey_get_keyname(termkey, keys[i]));
    778 	}
    779 }
    780 
    781 static bool cmd_help(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
    782 	if (!vis_window_new(vis, NULL))
    783 		return false;
    784 
    785 	Text *txt = vis->win->file->text;
    786 
    787 	text_appendf(txt, "vis %s (PID: %ld)\n\n", VERSION, (long)getpid());
    788 
    789 	text_appendf(txt, " Modes\n\n");
    790 	for (int i = 0; i < LENGTH(vis_modes); i++) {
    791 		Mode *mode = &vis_modes[i];
    792 		if (mode->help)
    793 			text_appendf(txt, "  %-18s\t%s\n", mode->name, mode->help);
    794 	}
    795 
    796 	if (!map_empty(vis->keymap)) {
    797 		text_appendf(txt, "\n Layout specific mappings (affects all modes except INSERT/REPLACE)\n\n");
    798 		map_iterate(vis->keymap, print_keylayout, txt);
    799 	}
    800 
    801 	print_mode(&vis_modes[VIS_MODE_NORMAL], txt);
    802 	print_mode(&vis_modes[VIS_MODE_OPERATOR_PENDING], txt);
    803 	print_mode(&vis_modes[VIS_MODE_VISUAL], txt);
    804 	print_mode(&vis_modes[VIS_MODE_INSERT], txt);
    805 
    806 	text_appendf(txt, "\n :-Commands\n\n");
    807 	map_iterate(vis->cmds, print_cmd, txt);
    808 
    809 	text_appendf(txt, "\n Marks\n\n");
    810 	text_appendf(txt, "  a-z General purpose marks\n");
    811 	for (size_t i = 0; i < LENGTH(vis_marks); i++) {
    812 		const char *help = VIS_HELP_USE(vis_marks[i].help);
    813 		text_appendf(txt, "  %c   %s\n", vis_marks[i].name, help ? help : "");
    814 	}
    815 
    816 	text_appendf(txt, "\n Registers\n\n");
    817 	text_appendf(txt, "  a-z General purpose registers\n");
    818 	text_appendf(txt, "  A-Z Append to corresponding general purpose register\n");
    819 	for (size_t i = 0; i < LENGTH(vis_registers); i++) {
    820 		const char *help = VIS_HELP_USE(vis_registers[i].help);
    821 		text_appendf(txt, "  %c   %s\n", vis_registers[i].name, help ? help : "");
    822 	}
    823 
    824 	text_appendf(txt, "\n :set command options\n\n");
    825 	map_iterate(vis->options, print_option, txt);
    826 
    827 	text_appendf(txt, "\n Key binding actions\n\n");
    828 	map_iterate(vis->actions, print_action, txt);
    829 
    830 	text_appendf(txt, "\n Symbolic keys usable for key bindings "
    831 		"(prefix with C-, S-, and M- for Ctrl, Shift and Alt respectively)\n\n");
    832 	print_symbolic_keys(vis, txt);
    833 
    834 	char *paths[] = { NULL, NULL };
    835 	char *paths_description[] = {
    836 		"Lua paths used to load runtime files (? will be replaced by filename):",
    837 		"Lua paths used to load C libraries (? will be replaced by filename):",
    838 	};
    839 
    840 	if (vis_lua_paths_get(vis, &paths[0], &paths[1])) {
    841 		for (size_t i = 0; i < LENGTH(paths); i++) {
    842 			text_appendf(txt, "\n %s\n\n", paths_description[i]);
    843 			for (char *elem = paths[i], *next; elem; elem = next) {
    844 				if ((next = strstr(elem, ";")))
    845 					*next++ = '\0';
    846 				if (*elem)
    847 					text_appendf(txt, "  %s\n", elem);
    848 			}
    849 			free(paths[i]);
    850 		}
    851 	}
    852 
    853 	text_appendf(txt, "\n Compile time configuration\n\n");
    854 
    855 	const struct {
    856 		const char *name;
    857 		bool enabled;
    858 	} configs[] = {
    859 		{ "Curses support: ", CONFIG_CURSES },
    860 		{ "Lua support: ", CONFIG_LUA },
    861 		{ "Lua LPeg statically built-in: ", CONFIG_LPEG },
    862 		{ "TRE based regex support: ", CONFIG_TRE },
    863 		{ "POSIX ACL support: ", CONFIG_ACL },
    864 		{ "SELinux support: ", CONFIG_SELINUX },
    865 	};
    866 
    867 	for (size_t i = 0; i < LENGTH(configs); i++)
    868 		text_appendf(txt, "  %-32s\t%s\n", configs[i].name, configs[i].enabled ? "yes" : "no");
    869 
    870 	text_save(txt, NULL);
    871 	view_cursors_to(vis->win->view.selection, 0);
    872 
    873 	if (argv[1])
    874 		vis_motion(vis, VIS_MOVE_SEARCH_FORWARD, argv[1]);
    875 	return true;
    876 }
    877 
    878 static bool cmd_langmap(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
    879 	const char *nonlatin = argv[1];
    880 	const char *latin = argv[2];
    881 	bool mapped = true;
    882 
    883 	if (!latin || !nonlatin) {
    884 		vis_info_show(vis, "usage: langmap <non-latin keys> <latin keys>");
    885 		return false;
    886 	}
    887 
    888 	while (*latin && *nonlatin) {
    889 		size_t i = 0, j = 0;
    890 		char latin_key[8], nonlatin_key[8];
    891 		do {
    892 			if (i < sizeof(latin_key)-1)
    893 				latin_key[i++] = *latin;
    894 			latin++;
    895 		} while (!ISUTF8(*latin));
    896 		do {
    897 			if (j < sizeof(nonlatin_key)-1)
    898 				nonlatin_key[j++] = *nonlatin;
    899 			nonlatin++;
    900 		} while (!ISUTF8(*nonlatin));
    901 		latin_key[i] = '\0';
    902 		nonlatin_key[j] = '\0';
    903 		mapped &= vis_keymap_add(vis, nonlatin_key, strdup(latin_key));
    904 	}
    905 
    906 	return mapped;
    907 }
    908 
    909 static bool cmd_map(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
    910 	bool mapped = false;
    911 	bool local = strstr(argv[0], "-") != NULL;
    912 	enum VisMode mode = vis_mode_from(vis, argv[1]);
    913 
    914 	if (local && !win) {
    915 		vis_info_show(vis, "Invalid window for :%s", argv[0]);
    916 		return false;
    917 	}
    918 
    919 	if (mode == VIS_MODE_INVALID || !argv[2] || !argv[3]) {
    920 		vis_info_show(vis, "usage: %s mode lhs rhs", argv[0]);
    921 		return false;
    922 	}
    923 
    924 	const char *lhs = argv[2];
    925 	KeyBinding *binding = vis_binding_new(vis);
    926 	if (!binding || !(binding->alias = strdup(argv[3])))
    927 		goto err;
    928 
    929 	if (local)
    930 		mapped = vis_window_mode_map(win, mode, cmd->flags == '!', lhs, binding);
    931 	else
    932 		mapped = vis_mode_map(vis, mode, cmd->flags == '!', lhs, binding);
    933 
    934 err:
    935 	if (!mapped) {
    936 		vis_info_show(vis, "Failed to map `%s' in %s mode%s", lhs, argv[1],
    937 		              cmd->flags != '!' ? ", mapping already exists, "
    938 		              "override with `!'" : "");
    939 		vis_binding_free(vis, binding);
    940 	}
    941 	return mapped;
    942 }
    943 
    944 static bool cmd_unmap(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) {
    945 	bool unmapped = false;
    946 	bool local = strstr(argv[0], "-") != NULL;
    947 	enum VisMode mode = vis_mode_from(vis, argv[1]);
    948 	const char *lhs = argv[2];
    949 
    950 	if (local && !win) {
    951 		vis_info_show(vis, "Invalid window for :%s", argv[0]);
    952 		return false;
    953 	}
    954 
    955 	if (mode == VIS_MODE_INVALID || !lhs) {
    956 		vis_info_show(vis, "usage: %s mode lhs", argv[0]);
    957 		return false;
    958 	}
    959 
    960 	if (local)
    961 		unmapped = vis_window_mode_unmap(win, mode, lhs);
    962 	else
    963 		unmapped = vis_mode_unmap(vis, mode, lhs);
    964 	if (!unmapped)
    965 		vis_info_show(vis, "Failed to unmap `%s' in %s mode", lhs, argv[1]);
    966 	return unmapped;
    967 }