Merge branch 'vis-open' of https://github.com/Pyrohh/vis into master
[vis.git] / vis-cmds.c
blob3a89d2a382a7acd599525ac157069a5e45c29986
1 #include <stdbool.h>
2 #include <string.h>
3 #include <strings.h>
4 #include <stdio.h>
5 #include <limits.h>
6 #include <errno.h>
7 #include <unistd.h>
8 #include <fcntl.h>
9 #include <ctype.h>
10 #include <sys/select.h>
11 #include <sys/types.h>
12 #include <sys/wait.h>
14 #include "vis-core.h"
15 #include "text-util.h"
16 #include "text-motions.h"
17 #include "util.h"
19 enum CmdOpt { /* option flags for command definitions */
20 CMD_OPT_NONE, /* no option (default value) */
21 CMD_OPT_FORCE, /* whether the command can be forced by appending '!' */
22 CMD_OPT_ARGS, /* whether the command line should be parsed in to space
23 * separated arguments to placed into argv, otherwise argv[1]
24 * will contain the remaining command line unmodified */
27 typedef struct { /* command definitions for the ':'-prompt */
28 const char *name[3]; /* name and optional alias for the command */
29 /* command logic called with a NULL terminated array of arguments.
30 * argv[0] will be the command name */
31 bool (*cmd)(Vis*, Filerange*, enum CmdOpt opt, const char *argv[]);
32 enum CmdOpt opt; /* command option flags */
33 } Command;
35 /** ':'-command implementations */
36 /* set various runtime options */
37 static bool cmd_set(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
38 /* for each argument create a new window and open the corresponding file */
39 static bool cmd_open(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
40 /* close current window (discard modifications if forced ) and open argv[1],
41 * if no argv[1] is given re-read to current file from disk */
42 static bool cmd_edit(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
43 /* close the current window, discard modifications if forced */
44 static bool cmd_quit(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
45 /* close all windows which show current file, discard modifications if forced */
46 static bool cmd_bdelete(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
47 /* close all windows, exit editor, discard modifications if forced */
48 static bool cmd_qall(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
49 /* for each argument try to insert the file content at current cursor postion */
50 static bool cmd_read(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
51 static bool cmd_substitute(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
52 /* if no argument are given, split the current window horizontally,
53 * otherwise open the file */
54 static bool cmd_split(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
55 /* if no argument are given, split the current window vertically,
56 * otherwise open the file */
57 static bool cmd_vsplit(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
58 /* create a new empty window and arrange all windows either horizontally or vertically */
59 static bool cmd_new(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
60 static bool cmd_vnew(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
61 /* save the file displayed in the current window and close it */
62 static bool cmd_wq(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
63 /* save the file displayed in the current window if it was changvis, then close the window */
64 static bool cmd_xit(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
65 /* save the file displayed in the current window to the name given.
66 * do not change internal filname association. further :w commands
67 * without arguments will still write to the old filename */
68 static bool cmd_write(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
69 /* save the file displayed in the current window to the name given,
70 * associate the new name with the buffer. further :w commands
71 * without arguments will write to the new filename */
72 static bool cmd_saveas(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
73 /* filter range through external program argv[1] */
74 static bool cmd_filter(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
75 /* switch to the previous/next saved state of the text, chronologically */
76 static bool cmd_earlier_later(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
77 /* dump current key bindings */
78 static bool cmd_help(Vis*, Filerange*, enum CmdOpt, const char *argv[]);
80 /* command recognized at the ':'-prompt. commands are found using a unique
81 * prefix match. that is if a command should be available under an abbreviation
82 * which is a prefix for another command it has to be added as an alias. the
83 * long human readable name should always come first */
84 static Command cmds[] = {
85 /* command name / optional alias, function, options */
86 { { "bdelete" }, cmd_bdelete, CMD_OPT_FORCE },
87 { { "edit" }, cmd_edit, CMD_OPT_FORCE },
88 { { "help" }, cmd_help, CMD_OPT_NONE },
89 { { "new" }, cmd_new, CMD_OPT_NONE },
90 { { "open" }, cmd_open, CMD_OPT_NONE },
91 { { "qall" }, cmd_qall, CMD_OPT_FORCE },
92 { { "quit", "q" }, cmd_quit, CMD_OPT_FORCE },
93 { { "read", }, cmd_read, CMD_OPT_FORCE },
94 { { "saveas" }, cmd_saveas, CMD_OPT_FORCE },
95 { { "set", }, cmd_set, CMD_OPT_ARGS },
96 { { "split" }, cmd_split, CMD_OPT_NONE },
97 { { "substitute", "s" }, cmd_substitute, CMD_OPT_NONE },
98 { { "vnew" }, cmd_vnew, CMD_OPT_NONE },
99 { { "vsplit", }, cmd_vsplit, CMD_OPT_NONE },
100 { { "wq", }, cmd_wq, CMD_OPT_FORCE },
101 { { "write", "w" }, cmd_write, CMD_OPT_FORCE },
102 { { "xit", }, cmd_xit, CMD_OPT_FORCE },
103 { { "earlier" }, cmd_earlier_later, CMD_OPT_NONE },
104 { { "later" }, cmd_earlier_later, CMD_OPT_NONE },
105 { { "!", }, cmd_filter, CMD_OPT_NONE },
106 { /* array terminator */ },
110 static void windows_arrange(Vis *vis, enum UiLayout layout) {
111 vis->ui->arrange(vis->ui, layout);
114 static void tabwidth_set(Vis *vis, int tabwidth) {
115 if (tabwidth < 1 || tabwidth > 8)
116 return;
117 for (Win *win = vis->windows; win; win = win->next)
118 view_tabwidth_set(win->view, tabwidth);
119 vis->tabwidth = tabwidth;
122 /* parse human-readable boolean value in s. If successful, store the result in
123 * outval and return true. Else return false and leave outval alone. */
124 static bool parse_bool(const char *s, bool *outval) {
125 for (const char **t = (const char*[]){"1", "true", "yes", "on", NULL}; *t; t++) {
126 if (!strcasecmp(s, *t)) {
127 *outval = true;
128 return true;
131 for (const char **f = (const char*[]){"0", "false", "no", "off", NULL}; *f; f++) {
132 if (!strcasecmp(s, *f)) {
133 *outval = false;
134 return true;
137 return false;
140 static bool cmd_set(Vis *vis, Filerange *range, enum CmdOpt cmdopt, const char *argv[]) {
142 typedef struct {
143 const char *names[3];
144 enum {
145 OPTION_TYPE_STRING,
146 OPTION_TYPE_BOOL,
147 OPTION_TYPE_NUMBER,
148 } type;
149 bool optional;
150 int index;
151 } OptionDef;
153 enum {
154 OPTION_AUTOINDENT,
155 OPTION_EXPANDTAB,
156 OPTION_TABWIDTH,
157 OPTION_SYNTAX,
158 OPTION_SHOW,
159 OPTION_NUMBER,
160 OPTION_NUMBER_RELATIVE,
161 OPTION_CURSOR_LINE,
162 OPTION_THEME,
163 OPTION_COLOR_COLUMN,
166 /* definitions have to be in the same order as the enum above */
167 static OptionDef options[] = {
168 [OPTION_AUTOINDENT] = { { "autoindent", "ai" }, OPTION_TYPE_BOOL },
169 [OPTION_EXPANDTAB] = { { "expandtab", "et" }, OPTION_TYPE_BOOL },
170 [OPTION_TABWIDTH] = { { "tabwidth", "tw" }, OPTION_TYPE_NUMBER },
171 [OPTION_SYNTAX] = { { "syntax" }, OPTION_TYPE_STRING, true },
172 [OPTION_SHOW] = { { "show" }, OPTION_TYPE_STRING },
173 [OPTION_NUMBER] = { { "numbers", "nu" }, OPTION_TYPE_BOOL },
174 [OPTION_NUMBER_RELATIVE] = { { "relativenumbers", "rnu" }, OPTION_TYPE_BOOL },
175 [OPTION_CURSOR_LINE] = { { "cursorline", "cul" }, OPTION_TYPE_BOOL },
176 [OPTION_THEME] = { { "theme" }, OPTION_TYPE_STRING },
177 [OPTION_COLOR_COLUMN] = { { "colorcolumn", "cc" }, OPTION_TYPE_NUMBER },
180 if (!vis->options) {
181 if (!(vis->options = map_new()))
182 return false;
183 for (int i = 0; i < LENGTH(options); i++) {
184 options[i].index = i;
185 for (const char **name = options[i].names; *name; name++) {
186 if (!map_put(vis->options, *name, &options[i]))
187 return false;
192 if (!argv[1]) {
193 vis_info_show(vis, "Expecting: set option [value]");
194 return false;
197 Arg arg;
198 bool invert = false;
199 OptionDef *opt = NULL;
201 if (!strncasecmp(argv[1], "no", 2)) {
202 opt = map_closest(vis->options, argv[1]+2);
203 if (opt && opt->type == OPTION_TYPE_BOOL)
204 invert = true;
205 else
206 opt = NULL;
209 if (!opt)
210 opt = map_closest(vis->options, argv[1]);
211 if (!opt) {
212 vis_info_show(vis, "Unknown option: `%s'", argv[1]);
213 return false;
216 switch (opt->type) {
217 case OPTION_TYPE_STRING:
218 if (!opt->optional && !argv[2]) {
219 vis_info_show(vis, "Expecting string option value");
220 return false;
222 arg.s = argv[2];
223 break;
224 case OPTION_TYPE_BOOL:
225 if (!argv[2]) {
226 arg.b = true;
227 } else if (!parse_bool(argv[2], &arg.b)) {
228 vis_info_show(vis, "Expecting boolean option value not: `%s'", argv[2]);
229 return false;
231 if (invert)
232 arg.b = !arg.b;
233 break;
234 case OPTION_TYPE_NUMBER:
235 if (!argv[2]) {
236 vis_info_show(vis, "Expecting number");
237 return false;
239 /* TODO: error checking? long type */
240 arg.i = strtoul(argv[2], NULL, 10);
241 break;
244 switch (opt->index) {
245 case OPTION_EXPANDTAB:
246 vis->expandtab = arg.b;
247 break;
248 case OPTION_AUTOINDENT:
249 vis->autoindent = arg.b;
250 break;
251 case OPTION_TABWIDTH:
252 tabwidth_set(vis, arg.i);
253 break;
254 case OPTION_SYNTAX:
255 if (!argv[2]) {
256 const char *syntax = view_syntax_get(vis->win->view);
257 if (syntax)
258 vis_info_show(vis, "Syntax definition in use: `%s'", syntax);
259 else
260 vis_info_show(vis, "No syntax definition in use");
261 return true;
264 if (parse_bool(argv[2], &arg.b) && !arg.b)
265 return view_syntax_set(vis->win->view, NULL);
266 if (!view_syntax_set(vis->win->view, argv[2])) {
267 vis_info_show(vis, "Unknown syntax definition: `%s'", argv[2]);
268 return false;
270 break;
271 case OPTION_SHOW:
272 if (!argv[2]) {
273 vis_info_show(vis, "Expecting: spaces, tabs, newlines");
274 return false;
276 char *keys[] = { "spaces", "tabs", "newlines" };
277 int values[] = {
278 UI_OPTION_SYMBOL_SPACE,
279 UI_OPTION_SYMBOL_TAB|UI_OPTION_SYMBOL_TAB_FILL,
280 UI_OPTION_SYMBOL_EOL,
282 int flags = view_options_get(vis->win->view);
283 for (const char **args = &argv[2]; *args; args++) {
284 for (int i = 0; i < LENGTH(keys); i++) {
285 if (strcmp(*args, keys[i]) == 0) {
286 flags |= values[i];
287 } else if (strstr(*args, keys[i]) == *args) {
288 bool show;
289 const char *v = *args + strlen(keys[i]);
290 if (*v == '=' && parse_bool(v+1, &show)) {
291 if (show)
292 flags |= values[i];
293 else
294 flags &= ~values[i];
299 view_options_set(vis->win->view, flags);
300 break;
301 case OPTION_NUMBER: {
302 enum UiOption opt = view_options_get(vis->win->view);
303 if (arg.b) {
304 opt &= ~UI_OPTION_LINE_NUMBERS_RELATIVE;
305 opt |= UI_OPTION_LINE_NUMBERS_ABSOLUTE;
306 } else {
307 opt &= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE;
309 view_options_set(vis->win->view, opt);
310 break;
312 case OPTION_NUMBER_RELATIVE: {
313 enum UiOption opt = view_options_get(vis->win->view);
314 if (arg.b) {
315 opt &= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE;
316 opt |= UI_OPTION_LINE_NUMBERS_RELATIVE;
317 } else {
318 opt &= ~UI_OPTION_LINE_NUMBERS_RELATIVE;
320 view_options_set(vis->win->view, opt);
321 break;
323 case OPTION_CURSOR_LINE: {
324 enum UiOption opt = view_options_get(vis->win->view);
325 if (arg.b)
326 opt |= UI_OPTION_CURSOR_LINE;
327 else
328 opt &= ~UI_OPTION_CURSOR_LINE;
329 view_options_set(vis->win->view, opt);
330 break;
332 case OPTION_THEME:
333 if (!vis_theme_load(vis, arg.s)) {
334 vis_info_show(vis, "Failed to load theme: `%s'", arg.s);
335 return false;
337 break;
338 case OPTION_COLOR_COLUMN:
339 view_colorcolumn_set(vis->win->view, arg.i);
340 break;
343 return true;
346 static bool is_file_pattern(const char *pattern) {
347 if (!pattern)
348 return false;
349 struct stat meta;
350 if (stat(pattern, &meta) == 0 && S_ISDIR(meta.st_mode))
351 return true;
352 return strchr(pattern, '*') || strchr(pattern, '[') || strchr(pattern, '{');
355 static const char *file_open_dialog(Vis *vis, const char *pattern) {
356 if (!is_file_pattern(pattern))
357 return pattern;
358 /* this is a bit of a hack, we temporarily replace the text/view of the active
359 * window such that we can use cmd_filter as is */
360 char vis_open[512];
361 static char filename[PATH_MAX];
362 Filerange range = text_range_empty();
363 Win *win = vis->win;
364 File *file = win->file;
365 Text *txt_orig = file->text;
366 View *view_orig = win->view;
367 Text *txt = text_load(NULL);
368 View *view = view_new(txt, NULL);
369 filename[0] = '\0';
370 snprintf(vis_open, sizeof(vis_open)-1, "vis-open %s", pattern ? pattern : "");
372 if (!txt || !view)
373 goto out;
374 win->view = view;
375 file->text = txt;
377 if (cmd_filter(vis, &range, CMD_OPT_NONE, (const char *[]){ "open", vis_open, NULL })) {
378 size_t len = text_size(txt);
379 if (len >= sizeof(filename))
380 len = 0;
381 if (len > 0)
382 text_bytes_get(txt, 0, --len, filename);
383 filename[len] = '\0';
386 out:
387 view_free(view);
388 text_free(txt);
389 win->view = view_orig;
390 file->text = txt_orig;
391 return filename[0] ? filename : NULL;
394 static bool openfiles(Vis *vis, const char **files) {
395 for (; *files; files++) {
396 const char *file = file_open_dialog(vis, *files);
397 if (!file)
398 return false;
399 errno = 0;
400 if (!vis_window_new(vis, file)) {
401 vis_info_show(vis, "Could not open `%s' %s", file,
402 errno ? strerror(errno) : "");
403 return false;
406 return true;
409 static bool cmd_open(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
410 if (!argv[1])
411 return vis_window_new(vis, NULL);
412 return openfiles(vis, &argv[1]);
415 static bool is_view_closeable(Win *win) {
416 if (!text_modified(win->file->text))
417 return true;
418 return win->file->refcount > 1;
421 static void info_unsaved_changes(Vis *vis) {
422 vis_info_show(vis, "No write since last change (add ! to override)");
425 static bool cmd_edit(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
426 Win *oldwin = vis->win;
427 if (!(opt & CMD_OPT_FORCE) && !is_view_closeable(oldwin)) {
428 info_unsaved_changes(vis);
429 return false;
431 if (!argv[1])
432 return vis_window_reload(oldwin);
433 if (!openfiles(vis, &argv[1]))
434 return false;
435 if (vis->win != oldwin)
436 vis_window_close(oldwin);
437 return vis->win != oldwin;
440 static bool cmd_quit(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
441 if (!(opt & CMD_OPT_FORCE) && !is_view_closeable(vis->win)) {
442 info_unsaved_changes(vis);
443 return false;
445 vis_window_close(vis->win);
446 if (!vis->windows)
447 vis_exit(vis, EXIT_SUCCESS);
448 return true;
451 static bool cmd_xit(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
452 if (text_modified(vis->win->file->text) && !cmd_write(vis, range, opt, argv)) {
453 if (!(opt & CMD_OPT_FORCE))
454 return false;
456 return cmd_quit(vis, range, opt, argv);
459 static bool cmd_bdelete(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
460 Text *txt = vis->win->file->text;
461 if (text_modified(txt) && !(opt & CMD_OPT_FORCE)) {
462 info_unsaved_changes(vis);
463 return false;
465 for (Win *next, *win = vis->windows; win; win = next) {
466 next = win->next;
467 if (win->file->text == txt)
468 vis_window_close(win);
470 if (!vis->windows)
471 vis_exit(vis, EXIT_SUCCESS);
472 return true;
475 static bool cmd_qall(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
476 for (Win *next, *win = vis->windows; win; win = next) {
477 next = win->next;
478 if (!text_modified(vis->win->file->text) || (opt & CMD_OPT_FORCE))
479 vis_window_close(win);
481 if (!vis->windows)
482 vis_exit(vis, EXIT_SUCCESS);
483 else
484 info_unsaved_changes(vis);
485 return vis->windows == NULL;
488 static bool cmd_read(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
489 char cmd[255];
491 if (!argv[1]) {
492 vis_info_show(vis, "Filename or command expected");
493 return false;
496 bool iscmd = (opt & CMD_OPT_FORCE) || argv[1][0] == '!';
497 const char *arg = argv[1]+(argv[1][0] == '!');
498 snprintf(cmd, sizeof cmd, "%s%s", iscmd ? "" : "cat ", arg);
500 size_t pos = view_cursor_get(vis->win->view);
501 if (!text_range_valid(range))
502 *range = (Filerange){ .start = pos, .end = pos };
503 Filerange delete = *range;
504 range->start = range->end;
506 bool ret = cmd_filter(vis, range, opt, (const char*[]){ argv[0], "sh", "-c", cmd, NULL});
507 if (ret)
508 text_delete_range(vis->win->file->text, &delete);
509 return ret;
512 static bool cmd_substitute(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
513 char pattern[255];
514 if (!text_range_valid(range))
515 *range = (Filerange){ .start = 0, .end = text_size(vis->win->file->text) };
516 snprintf(pattern, sizeof pattern, "s%s", argv[1]);
517 return cmd_filter(vis, range, opt, (const char*[]){ argv[0], "sed", pattern, NULL});
520 static bool cmd_split(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
521 enum UiOption options = view_options_get(vis->win->view);
522 windows_arrange(vis, UI_LAYOUT_HORIZONTAL);
523 if (!argv[1])
524 return vis_window_split(vis->win);
525 bool ret = openfiles(vis, &argv[1]);
526 view_options_set(vis->win->view, options);
527 return ret;
530 static bool cmd_vsplit(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
531 enum UiOption options = view_options_get(vis->win->view);
532 windows_arrange(vis, UI_LAYOUT_VERTICAL);
533 if (!argv[1])
534 return vis_window_split(vis->win);
535 bool ret = openfiles(vis, &argv[1]);
536 view_options_set(vis->win->view, options);
537 return ret;
540 static bool cmd_new(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
541 windows_arrange(vis, UI_LAYOUT_HORIZONTAL);
542 return vis_window_new(vis, NULL);
545 static bool cmd_vnew(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
546 windows_arrange(vis, UI_LAYOUT_VERTICAL);
547 return vis_window_new(vis, NULL);
550 static bool cmd_wq(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
551 if (cmd_write(vis, range, opt, argv))
552 return cmd_quit(vis, range, opt, argv);
553 return false;
556 static bool cmd_write(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
557 File *file = vis->win->file;
558 Text *text = file->text;
559 if (!text_range_valid(range))
560 *range = (Filerange){ .start = 0, .end = text_size(text) };
561 if (!argv[1])
562 argv[1] = file->name;
563 if (!argv[1]) {
564 if (file->is_stdin) {
565 if (strchr(argv[0], 'q')) {
566 ssize_t written = text_write_range(text, range, STDOUT_FILENO);
567 if (written == -1 || (size_t)written != text_range_size(range)) {
568 vis_info_show(vis, "Can not write to stdout");
569 return false;
571 /* make sure the file is marked as saved i.e. not modified */
572 text_save_range(text, range, NULL);
573 return true;
575 vis_info_show(vis, "No filename given, use 'wq' to write to stdout");
576 return false;
578 vis_info_show(vis, "Filename expected");
579 return false;
581 for (const char **name = &argv[1]; *name; name++) {
582 struct stat meta;
583 if (!(opt & CMD_OPT_FORCE) && file->stat.st_mtime && stat(*name, &meta) == 0 &&
584 file->stat.st_mtime < meta.st_mtime) {
585 vis_info_show(vis, "WARNING: file has been changed since reading it");
586 return false;
588 if (!text_save_range(text, range, *name)) {
589 vis_info_show(vis, "Can't write `%s'", *name);
590 return false;
592 if (!file->name) {
593 vis_window_name(vis->win, *name);
594 file->name = vis->win->file->name;
596 if (strcmp(file->name, *name) == 0)
597 file->stat = text_stat(text);
598 if (vis->event && vis->event->file_save)
599 vis->event->file_save(vis, file);
601 return true;
604 static bool cmd_saveas(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
605 if (cmd_write(vis, range, opt, argv)) {
606 vis_window_name(vis->win, argv[1]);
607 vis->win->file->stat = text_stat(vis->win->file->text);
608 return true;
610 return false;
613 static bool cmd_filter(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
614 /* if an invalid range was given, stdin (i.e. key board input) is passed
615 * through the external command. */
616 Text *text = vis->win->file->text;
617 View *view = vis->win->view;
618 int pin[2], pout[2], perr[2], status = -1;
619 bool interactive = !text_range_valid(range);
620 size_t pos = view_cursor_get(view);
622 if (pipe(pin) == -1)
623 return false;
624 if (pipe(pout) == -1) {
625 close(pin[0]);
626 close(pin[1]);
627 return false;
630 if (pipe(perr) == -1) {
631 close(pin[0]);
632 close(pin[1]);
633 close(pout[0]);
634 close(pout[1]);
635 return false;
638 vis->ui->terminal_save(vis->ui);
639 pid_t pid = fork();
641 if (pid == -1) {
642 close(pin[0]);
643 close(pin[1]);
644 close(pout[0]);
645 close(pout[1]);
646 close(perr[0]);
647 close(perr[1]);
648 vis_info_show(vis, "fork failure: %s", strerror(errno));
649 return false;
650 } else if (pid == 0) { /* child i.e filter */
651 if (!interactive)
652 dup2(pin[0], STDIN_FILENO);
653 close(pin[0]);
654 close(pin[1]);
655 dup2(pout[1], STDOUT_FILENO);
656 close(pout[1]);
657 close(pout[0]);
658 if (!interactive)
659 dup2(perr[1], STDERR_FILENO);
660 close(perr[0]);
661 close(perr[1]);
662 if (!argv[2])
663 execl("/bin/sh", "sh", "-c", argv[1], NULL);
664 else
665 execvp(argv[1], (char**)argv+1);
666 vis_info_show(vis, "exec failure: %s", strerror(errno));
667 exit(EXIT_FAILURE);
670 vis->cancel_filter = false;
672 close(pin[0]);
673 close(pout[1]);
674 close(perr[1]);
676 fcntl(pout[0], F_SETFL, O_NONBLOCK);
677 fcntl(perr[0], F_SETFL, O_NONBLOCK);
679 if (interactive)
680 *range = (Filerange){ .start = pos, .end = pos };
682 /* ranges which are written to the filter and read back in */
683 Filerange rout = *range;
684 Filerange rin = (Filerange){ .start = range->end, .end = range->end };
686 /* The general idea is the following:
688 * 1) take a snapshot
689 * 2) write [range.start, range.end] to exteneral command
690 * 3) read the output of the external command and insert it after the range
691 * 4) depending on the exit status of the external command
692 * - on success: delete original range
693 * - on failure: revert to previous snapshot
695 * 2) and 3) happend in small junks
698 text_snapshot(text);
700 fd_set rfds, wfds;
701 Buffer errmsg;
702 buffer_init(&errmsg);
704 do {
705 if (vis->cancel_filter) {
706 kill(-pid, SIGTERM);
707 vis_info_show(vis, "Command cancelled");
708 break;
711 FD_ZERO(&rfds);
712 FD_ZERO(&wfds);
713 if (pin[1] != -1)
714 FD_SET(pin[1], &wfds);
715 if (pout[0] != -1)
716 FD_SET(pout[0], &rfds);
717 if (perr[0] != -1)
718 FD_SET(perr[0], &rfds);
720 if (select(FD_SETSIZE, &rfds, &wfds, NULL, NULL) == -1) {
721 if (errno == EINTR)
722 continue;
723 vis_info_show(vis, "Select failure");
724 break;
727 if (pin[1] != -1 && FD_ISSET(pin[1], &wfds)) {
728 Filerange junk = *range;
729 if (junk.end > junk.start + PIPE_BUF)
730 junk.end = junk.start + PIPE_BUF;
731 ssize_t len = text_write_range(text, &junk, pin[1]);
732 if (len > 0) {
733 range->start += len;
734 if (text_range_size(range) == 0) {
735 close(pout[1]);
736 pout[1] = -1;
738 } else {
739 close(pin[1]);
740 pin[1] = -1;
741 if (len == -1)
742 vis_info_show(vis, "Error writing to external command");
746 if (pout[0] != -1 && FD_ISSET(pout[0], &rfds)) {
747 char buf[BUFSIZ];
748 ssize_t len = read(pout[0], buf, sizeof buf);
749 if (len > 0) {
750 text_insert(text, rin.end, buf, len);
751 rin.end += len;
752 } else if (len == 0) {
753 close(pout[0]);
754 pout[0] = -1;
755 } else if (errno != EINTR && errno != EWOULDBLOCK) {
756 vis_info_show(vis, "Error reading from filter stdout");
757 close(pout[0]);
758 pout[0] = -1;
762 if (perr[0] != -1 && FD_ISSET(perr[0], &rfds)) {
763 char buf[BUFSIZ];
764 ssize_t len = read(perr[0], buf, sizeof buf);
765 if (len > 0) {
766 buffer_append(&errmsg, buf, len);
767 } else if (len == 0) {
768 close(perr[0]);
769 perr[0] = -1;
770 } else if (errno != EINTR && errno != EWOULDBLOCK) {
771 vis_info_show(vis, "Error reading from filter stderr");
772 close(perr[0]);
773 perr[0] = -1;
777 } while (pin[1] != -1 || pout[0] != -1 || perr[0] != -1);
779 if (pin[1] != -1)
780 close(pin[1]);
781 if (pout[0] != -1)
782 close(pout[0]);
783 if (perr[0] != -1)
784 close(perr[0]);
786 if (waitpid(pid, &status, 0) == pid && status == 0) {
787 text_delete_range(text, &rout);
788 text_snapshot(text);
789 } else {
790 /* make sure we have somehting to undo */
791 text_insert(text, pos, " ", 1);
792 text_undo(text);
795 view_cursor_to(view, rout.start);
797 if (!vis->cancel_filter) {
798 if (status == 0)
799 vis_info_show(vis, "Command succeded");
800 else if (errmsg.len > 0)
801 vis_info_show(vis, "Command failed: %s", errmsg.data);
802 else
803 vis_info_show(vis, "Command failed");
806 vis->ui->terminal_restore(vis->ui);
807 return status == 0;
810 static bool cmd_earlier_later(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
811 Text *txt = vis->win->file->text;
812 char *unit = "";
813 long count = 1;
814 size_t pos = EPOS;
815 if (argv[1]) {
816 errno = 0;
817 count = strtol(argv[1], &unit, 10);
818 if (errno || unit == argv[1] || count < 0) {
819 vis_info_show(vis, "Invalid number");
820 return false;
823 if (*unit) {
824 while (*unit && isspace((unsigned char)*unit))
825 unit++;
826 switch (*unit) {
827 case 'd': count *= 24; /* fall through */
828 case 'h': count *= 60; /* fall through */
829 case 'm': count *= 60; /* fall through */
830 case 's': break;
831 default:
832 vis_info_show(vis, "Unknown time specifier (use: s,m,h or d)");
833 return false;
836 if (argv[0][0] == 'e')
837 count = -count; /* earlier, move back in time */
839 pos = text_restore(txt, text_state(txt) + count);
843 if (!*unit) {
844 if (argv[0][0] == 'e')
845 pos = text_earlier(txt, count);
846 else
847 pos = text_later(txt, count);
850 time_t state = text_state(txt);
851 char buf[32];
852 strftime(buf, sizeof buf, "State from %H:%M", localtime(&state));
853 vis_info_show(vis, "%s", buf);
855 return pos != EPOS;
858 bool print_keybinding(const char *key, void *value, void *data) {
859 Text *txt = (Text*)data;
860 KeyBinding *binding = (KeyBinding*)value;
861 const char *desc = binding->alias;
862 if (!desc && binding->action)
863 desc = binding->action->help;
864 return text_appendf(txt, " %-15s\t%s\n", key, desc ? desc : "");
867 static void print_mode(Mode *mode, Text *txt, bool recursive) {
868 if (recursive && mode->parent)
869 print_mode(mode->parent, txt, recursive);
870 map_iterate(mode->bindings, print_keybinding, txt);
873 static bool cmd_help(Vis *vis, Filerange *range, enum CmdOpt opt, const char *argv[]) {
874 if (!vis_window_new(vis, NULL))
875 return false;
877 Text *txt = vis->win->file->text;
879 text_appendf(txt, "vis %s, compiled " __DATE__ " " __TIME__ "\n\n", VERSION);
881 text_appendf(txt, " Modes\n\n");
882 for (int i = 0; i < LENGTH(vis_modes); i++) {
883 Mode *mode = &vis_modes[i];
884 if (mode->help)
885 text_appendf(txt, " %-15s\t%s\n", mode->name, mode->help);
888 for (int i = 0; i < LENGTH(vis_modes); i++) {
889 Mode *mode = &vis_modes[i];
890 if (mode->isuser && !map_empty(mode->bindings)) {
891 text_appendf(txt, "\n %s\n\n", mode->name);
892 print_mode(mode, txt, i == VIS_MODE_NORMAL ||
893 i == VIS_MODE_INSERT);
897 text_appendf(txt, "\n Text objects\n\n");
898 print_mode(&vis_modes[VIS_MODE_TEXTOBJ], txt, false);
900 text_appendf(txt, "\n Motions\n\n");
901 print_mode(&vis_modes[VIS_MODE_MOVE], txt, false);
903 text_appendf(txt, "\n :-Commands\n\n");
904 for (Command *cmd = cmds; cmd && cmd->name[0]; cmd++)
905 text_appendf(txt, " %s\n", cmd->name[0]);
907 text_save(txt, NULL);
908 return true;
911 static Filepos parse_pos(Win *win, char **cmd) {
912 size_t pos = EPOS;
913 View *view = win->view;
914 Text *txt = win->file->text;
915 Mark *marks = win->file->marks;
916 switch (**cmd) {
917 case '.':
918 pos = text_line_begin(txt, view_cursor_get(view));
919 (*cmd)++;
920 break;
921 case '$':
922 pos = text_size(txt);
923 (*cmd)++;
924 break;
925 case '\'':
926 (*cmd)++;
927 if ('a' <= **cmd && **cmd <= 'z')
928 pos = text_mark_get(txt, marks[**cmd - 'a']);
929 else if (**cmd == '<')
930 pos = text_mark_get(txt, marks[VIS_MARK_SELECTION_START]);
931 else if (**cmd == '>')
932 pos = text_mark_get(txt, marks[VIS_MARK_SELECTION_END]);
933 (*cmd)++;
934 break;
935 case '/':
936 (*cmd)++;
937 char *pattern_end = strchr(*cmd, '/');
938 if (!pattern_end)
939 return EPOS;
940 *pattern_end++ = '\0';
941 Regex *regex = text_regex_new();
942 if (!regex)
943 return EPOS;
944 if (!text_regex_compile(regex, *cmd, 0)) {
945 *cmd = pattern_end;
946 pos = text_search_forward(txt, view_cursor_get(view), regex);
948 text_regex_free(regex);
949 break;
950 case '+':
951 case '-':
953 CursorPos curspos = view_cursor_getpos(view);
954 long long line = curspos.line + strtoll(*cmd, cmd, 10);
955 if (line < 0)
956 line = 0;
957 pos = text_pos_by_lineno(txt, line);
958 break;
960 default:
961 if ('0' <= **cmd && **cmd <= '9')
962 pos = text_pos_by_lineno(txt, strtoul(*cmd, cmd, 10));
963 break;
966 return pos;
969 static Filerange parse_range(Win *win, char **cmd) {
970 Text *txt = win->file->text;
971 Filerange r = text_range_empty();
972 Mark *marks = win->file->marks;
973 char start = **cmd;
974 switch (**cmd) {
975 case '%':
976 r.start = 0;
977 r.end = text_size(txt);
978 (*cmd)++;
979 break;
980 case '*':
981 r.start = text_mark_get(txt, marks[VIS_MARK_SELECTION_START]);
982 r.end = text_mark_get(txt, marks[VIS_MARK_SELECTION_END]);
983 (*cmd)++;
984 break;
985 default:
986 r.start = parse_pos(win, cmd);
987 if (**cmd != ',') {
988 if (start == '.')
989 r.end = text_line_next(txt, r.start);
990 return r;
992 (*cmd)++;
993 r.end = parse_pos(win, cmd);
994 break;
996 return r;
999 static Command *lookup_cmd(Vis *vis, const char *name) {
1000 if (!vis->cmds) {
1001 if (!(vis->cmds = map_new()))
1002 return NULL;
1004 for (Command *cmd = cmds; cmd && cmd->name[0]; cmd++) {
1005 for (const char **name = cmd->name; *name; name++)
1006 map_put(vis->cmds, *name, cmd);
1009 return map_closest(vis->cmds, name);
1012 bool vis_cmd(Vis *vis, const char *cmdline) {
1013 enum CmdOpt opt = CMD_OPT_NONE;
1014 size_t len = strlen(cmdline);
1015 char *line = malloc(len+2);
1016 if (!line)
1017 return false;
1018 line = strncpy(line, cmdline, len+1);
1019 char *name = line;
1021 Filerange range = parse_range(vis->win, &name);
1022 if (!text_range_valid(&range)) {
1023 /* if only one position was given, jump to it */
1024 if (range.start != EPOS && !*name) {
1025 view_cursor_to(vis->win->view, range.start);
1026 free(line);
1027 return true;
1030 if (name != line) {
1031 vis_info_show(vis, "Invalid range\n");
1032 free(line);
1033 return false;
1036 /* skip leading white space */
1037 while (*name == ' ')
1038 name++;
1039 char *param = name;
1040 while (*param && isalpha(*param))
1041 param++;
1043 if (*param == '!') {
1044 if (param != name) {
1045 opt |= CMD_OPT_FORCE;
1046 *param = ' ';
1047 } else {
1048 param++;
1052 memmove(param+1, param, strlen(param)+1);
1053 *param++ = '\0'; /* separate command name from parameters */
1055 Command *cmd = lookup_cmd(vis, name);
1056 if (!cmd) {
1057 vis_info_show(vis, "Not an editor command");
1058 free(line);
1059 return false;
1062 char *s = param;
1063 const char *argv[32] = { name };
1064 for (int i = 1; i < LENGTH(argv); i++) {
1065 while (s && *s && *s == ' ')
1066 s++;
1067 if (s && !*s)
1068 s = NULL;
1069 argv[i] = s;
1070 if (!(cmd->opt & CMD_OPT_ARGS)) {
1071 /* remove trailing spaces */
1072 if (s) {
1073 while (*s) s++;
1074 while (*(--s) == ' ') *s = '\0';
1076 s = NULL;
1078 if (s && (s = strchr(s, ' ')))
1079 *s++ = '\0';
1080 /* strip out a single '!' argument to make ":q !" work */
1081 if (argv[i] && !strcmp(argv[i], "!")) {
1082 opt |= CMD_OPT_FORCE;
1083 i--;
1087 cmd->cmd(vis, &range, opt, argv);
1088 free(line);
1089 return true;