diff options
| author | Marc André Tanner <mat@brain-dump.org> | 2014-08-24 10:04:28 +0200 |
|---|---|---|
| committer | Marc André Tanner <mat@brain-dump.org> | 2014-08-24 10:05:37 +0200 |
| commit | bc0f09dce9fb9420ea1d5c10ebfacf50916b10af (patch) | |
| tree | fdd6c4d7c791bcb540c11508b5007721f9c10c7f | |
| parent | 6f8cd49d22650ea3c91f0b96b2b576104fd0670c (diff) | |
| download | vis-bc0f09dce9fb9420ea1d5c10ebfacf50916b10af.tar.gz vis-bc0f09dce9fb9420ea1d5c10ebfacf50916b10af.tar.xz | |
Add work in progress editor frontend
| -rw-r--r-- | LICENSE | 13 | ||||
| -rw-r--r-- | Makefile | 63 | ||||
| -rw-r--r-- | colors.c | 106 | ||||
| -rw-r--r-- | config.def.h | 355 | ||||
| -rw-r--r-- | config.mk | 17 | ||||
| -rw-r--r-- | editor.c | 1305 | ||||
| -rw-r--r-- | editor.h | 104 | ||||
| -rw-r--r-- | vis.c | 264 |
8 files changed, 2227 insertions, 0 deletions
@@ -0,0 +1,13 @@ +Copyright (c) 2014 Marc André Tanner <mat at brain-dump.org> + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2bcc82e --- /dev/null +++ b/Makefile @@ -0,0 +1,63 @@ +include config.mk + +SRC += vis.c colors.c editor.c text.c +OBJ = ${SRC:.c=.o} + +all: clean options vis + +options: + @echo vis build options: + @echo "CFLAGS = ${CFLAGS}" + @echo "LDFLAGS = ${LDFLAGS}" + @echo "CC = ${CC}" + +config.h: +#TODO cp config.def.h config.h + ln -fs config.def.h config.h + +.c.o: + @echo CC $< + @${CC} -c ${CFLAGS} $< + +${OBJ}: config.h config.mk + +vis: ${OBJ} + @echo CC -o $@ + @${CC} -o $@ ${OBJ} ${LDFLAGS} + @ln -sf $@ nano + +debug: clean + @make CFLAGS='${DEBUG_CFLAGS}' + +clean: + @echo cleaning + @rm -f vis nano ${OBJ} vis-${VERSION}.tar.gz + +dist: clean + @echo creating dist tarball + @mkdir -p vis-${VERSION} + @cp -R LICENSE Makefile README config.def.h config.mk \ + ${SRC} editor.h text.h util.h vis.1 vis-${VERSION} + @tar -cf vis-${VERSION}.tar vis-${VERSION} + @gzip vis-${VERSION}.tar + @rm -rf vis-${VERSION} + +install: vis + @echo stripping executable + @strip -s vis + @echo installing executable file to ${DESTDIR}${PREFIX}/bin + @mkdir -p ${DESTDIR}${PREFIX}/bin + @cp -f vis ${DESTDIR}${PREFIX}/bin + @chmod 755 ${DESTDIR}${PREFIX}/bin/vis + @echo installing manual page to ${DESTDIR}${MANPREFIX}/man1 + @mkdir -p ${DESTDIR}${MANPREFIX}/man1 + @sed "s/VERSION/${VERSION}/g" < vis.1 > ${DESTDIR}${MANPREFIX}/man1/vis.1 + @chmod 644 ${DESTDIR}${MANPREFIX}/man1/vis.1 + +uninstall: + @echo removing executable file from ${DESTDIR}${PREFIX}/bin + @rm -f ${DESTDIR}${PREFIX}/bin/vis + @echo removing manual page from ${DESTDIR}${MANPREFIX}/man1 + @rm -f ${DESTDIR}${MANPREFIX}/man1/vis.1 + +.PHONY: all options clean dist install uninstall debug diff --git a/colors.c b/colors.c new file mode 100644 index 0000000..d1a0357 --- /dev/null +++ b/colors.c @@ -0,0 +1,106 @@ +#include <stdbool.h> +#include <stdlib.h> +#include <curses.h> + +#include "editor.h" +#include "util.h" + +#ifdef NCURSES_VERSION +# ifndef NCURSES_EXT_COLORS +# define NCURSES_EXT_COLORS 0 +# endif +# if !NCURSES_EXT_COLORS +# define MAX_COLOR_PAIRS 256 +# endif +#endif +#ifndef MAX_COLOR_PAIRS +# define MAX_COLOR_PAIRS COLOR_PAIRS +#endif + +static bool has_default_colors; +static short *color2palette, default_fg, default_bg; +static short color_pairs_reserved, color_pairs_max, color_pair_current; + +static unsigned int color_hash(short fg, short bg) +{ + if (fg == -1) + fg = COLORS; + if (bg == -1) + bg = COLORS + 1; + return fg * (COLORS + 2) + bg; +} + +short editor_color_get(short fg, short bg) +{ + if (fg >= COLORS) + fg = default_fg; + if (bg >= COLORS) + bg = default_bg; + + if (!has_default_colors) { + if (fg == -1) + fg = default_fg; + if (bg == -1) + bg = default_bg; + } + + if (!color2palette || (fg == -1 && bg == -1)) + return 0; + unsigned int index = color_hash(fg, bg); + if (color2palette[index] == 0) { + short oldfg, oldbg; + for (;;) { + if (++color_pair_current >= color_pairs_max) + color_pair_current = color_pairs_reserved + 1; + pair_content(color_pair_current, &oldfg, &oldbg); + unsigned int old_index = color_hash(oldfg, oldbg); + if (color2palette[old_index] >= 0) { + if (init_pair(color_pair_current, fg, bg) == OK) { + color2palette[old_index] = 0; + color2palette[index] = color_pair_current; + } + break; + } + } + } + + short color_pair = color2palette[index]; + return color_pair >= 0 ? color_pair : -color_pair; +} + +short editor_color_reserve(short fg, short bg) +{ + if (!color2palette) + editor_init(); + if (!color2palette || fg >= COLORS || bg >= COLORS) + return 0; + if (!has_default_colors && fg == -1) + fg = default_fg; + if (!has_default_colors && bg == -1) + bg = default_bg; + if (fg == -1 && bg == -1) + return 0; + unsigned int index = color_hash(fg, bg); + if (color2palette[index] >= 0) { + if (init_pair(++color_pairs_reserved, fg, bg) == OK) + color2palette[index] = -color_pairs_reserved; + } + short color_pair = color2palette[index]; + return color_pair >= 0 ? color_pair : -color_pair; +} + +void editor_init(void) +{ + if (color2palette) + return; + pair_content(0, &default_fg, &default_bg); + if (default_fg == -1) + default_fg = COLOR_WHITE; + if (default_bg == -1) + default_bg = COLOR_BLACK; + has_default_colors = (use_default_colors() == OK); + color_pairs_max = MIN(COLOR_PAIRS, MAX_COLOR_PAIRS); + if (COLORS) + color2palette = calloc((COLORS + 2) * (COLORS + 2), sizeof(short)); + editor_color_reserve(COLOR_WHITE, COLOR_BLACK); +} diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..eef2d55 --- /dev/null +++ b/config.def.h @@ -0,0 +1,355 @@ +#define ESC 0x1B +#define NONE(k) { .str = { k }, .code = 0 } +#define KEY(k) { .str = { '\0' }, .code = KEY_##k } +#define CONTROL(k) NONE((k)&0x1F) +#define META(k) { .str = { ESC, (k) }, .code = 0 } +#define BACKSPACE(func, arg) \ + { { KEY(BACKSPACE) }, (func), { .m = (arg) } }, \ + { { CONTROL('H') }, (func), { .m = (arg) } }, \ + { { NONE(127) }, (func), { .m = (arg) } }, \ + { { CONTROL('B') }, (func), { .m = (arg) } } + +/* draw a statubar, do whatever you want with the given curses window */ +static void statusbar(WINDOW *win, bool active, const char *filename, int line, int col) { + int width, height; + getmaxyx(win, height, width); + (void)height; + wattrset(win, active ? A_REVERSE|A_BOLD : A_REVERSE); + mvwhline(win, 0, 0, ' ', width); + mvwprintw(win, 0, 0, "%s", filename); + char buf[width + 1]; + int len = snprintf(buf, width, "%d, %d", line, col); + if (len > 0) { + buf[len] = '\0'; + mvwaddstr(win, 0, width - len - 1, buf); + } +} + +static void switchmode(const Arg *arg); +enum { + VIS_MODE_BASIC, + VIS_MODE_MOVE, + VIS_MODE_NORMAL, + VIS_MODE_VISUAL, + VIS_MODE_INSERT, + VIS_MODE_REPLACE, +}; + +void quit(const Arg *arg) { + endwin(); + exit(0); +} + +static void split(const Arg *arg) { + editor_window_split(editor, arg->s); +} + +static void mark_set(const Arg *arg) { + editor_mark_set(editor, arg->i, editor_cursor_get(editor)); +} + +static void mark_goto(const Arg *arg) { + editor_mark_goto(editor, arg->i); +} +/* use vim's + :help motion + :h operator + :h text-objects + as reference +*/ + +static KeyBinding movement[] = { + { { KEY(LEFT) }, cursor, { .m = editor_char_prev } }, + { { KEY(RIGHT) }, cursor, { .m = editor_char_next } }, + { { KEY(UP) }, cursor, { .m = editor_line_up } }, + { { KEY(DOWN) }, cursor, { .m = editor_line_down } }, + { { KEY(PPAGE) }, cursor, { .m = editor_page_up } }, + { { KEY(NPAGE) }, cursor, { .m = editor_page_down } }, + { { KEY(HOME) }, cursor, { .m = editor_line_start } }, + { { KEY(END) }, cursor, { .m = editor_line_end } }, + // temporary until we have a way to enter user commands + { { CONTROL('c') }, quit, }, + { /* empty last element, array terminator */ }, +}; + +static KeyBinding vis_movement[] = { + BACKSPACE( cursor, editor_char_prev ), + { { NONE(' ') }, cursor, { .m = editor_char_next } }, + { { CONTROL('w'), NONE('c') }, split, { .s = NULL } }, + { { CONTROL('w'), NONE('j') }, call, { .f = editor_window_next } }, + { { CONTROL('w'), NONE('k') }, call, { .f = editor_window_prev } }, + { { NONE('h') }, cursor, { .m = editor_char_prev } }, + { { NONE('l') }, cursor, { .m = editor_char_next } }, + { { NONE('k') }, cursor, { .m = editor_line_up } }, + { { CONTROL('P') }, cursor, { .m = editor_line_up } }, + { { NONE('j') }, cursor, { .m = editor_line_down } }, + { { CONTROL('J') }, cursor, { .m = editor_line_down } }, + { { CONTROL('N') }, cursor, { .m = editor_line_down } }, + { { KEY(ENTER) }, cursor, { .m = editor_line_down } }, + { { NONE('0') }, cursor, { .m = editor_line_begin } }, + { { NONE('^') }, cursor, { .m = editor_line_start } }, + { { NONE('g'), NONE('_') }, cursor, { .m = editor_line_finish } }, + { { NONE('$') }, cursor, { .m = editor_line_end } }, + { { CONTROL('F') }, cursor, { .m = editor_page_up } }, + { { CONTROL('B') }, cursor, { .m = editor_page_down } }, + { { NONE('%') }, cursor, { .m = editor_bracket_match } }, + { { NONE('b') }, cursor, { .m = editor_word_start_prev } }, + { { KEY(SLEFT) }, cursor, { .m = editor_word_start_prev } }, + { { NONE('w') }, cursor, { .m = editor_word_start_next } }, + { { KEY(SRIGHT) }, cursor, { .m = editor_word_start_next } }, + { { NONE('g'), NONE('e') }, cursor, { .m = editor_word_end_prev } }, + { { NONE('e') }, cursor, { .m = editor_word_end_next } }, + { { NONE('}') }, cursor, { .m = editor_paragraph_next } }, + { { NONE('{') }, cursor, { .m = editor_paragraph_prev } }, + { { NONE(')') }, cursor, { .m = editor_sentence_next } }, + { { NONE('(') }, cursor, { .m = editor_sentence_prev } }, + { { NONE('g'), NONE('g') }, cursor, { .m = editor_file_begin } }, + { { NONE('G') }, cursor, { .m = editor_file_end } }, + { /* empty last element, array terminator */ }, +}; + +static KeyBinding vis_normal[] = { + { { NONE('x') }, cursor, { .m = editor_delete } }, + { { NONE('i') }, switchmode, { .i = VIS_MODE_INSERT } }, + { { NONE('v') }, switchmode, { .i = VIS_MODE_VISUAL } }, + { { NONE('R') }, switchmode, { .i = VIS_MODE_REPLACE} }, + { { NONE('u') }, call, { .f = editor_undo } }, + { { CONTROL('R') }, call, { .f = editor_redo } }, + { { CONTROL('L') }, call, { .f = editor_draw } }, + // DEMO STUFF + { { NONE('n') }, find_forward, { .s = "if" } }, + { { NONE('p') }, find_backward, { .s = "if" } }, + { { NONE('5') }, line, { .i = 50 } }, + { { NONE('s') }, mark_set, { .i = 0 } }, + { { NONE('9') }, mark_goto, { .i = 0 } }, + { /* empty last element, array terminator */ }, +}; + +static KeyBinding vis_visual[] = { + { { NONE(ESC) }, switchmode, { .i = VIS_MODE_NORMAL } }, + { /* empty last element, array terminator */ }, +}; + +static void vis_visual_enter(void) { + editor_selection_start(editor); +} + +static void vis_visual_leave(void) { + editor_selection_clear(editor); +} + +static KeyBinding vis_insert[] = { + { { NONE(ESC) }, switchmode, { .i = VIS_MODE_NORMAL } }, + { { CONTROL('D') }, cursor, { .m = editor_delete } }, + BACKSPACE( cursor, editor_backspace ), + { /* empty last element, array terminator */ }, +}; + +static bool vis_insert_input(const char *str, size_t len) { + editor_insert(editor, str, len); + return true; +} + +static KeyBinding vis_replace[] = { + { { NONE(ESC) }, switchmode, { .i = VIS_MODE_NORMAL } }, + { { CONTROL('D') }, cursor, { .m = editor_delete } }, + BACKSPACE( cursor, editor_backspace ), + { /* empty last element, array terminator */ }, +}; + +static bool vis_replace_input(const char *str, size_t len) { + editor_replace(editor, str, len); + return true; +} + +static Mode vis[] = { + [VIS_MODE_BASIC] = { + .parent = NULL, + .bindings = movement, + }, + [VIS_MODE_MOVE] = { + .parent = &vis[VIS_MODE_BASIC], + .bindings = vis_movement, + }, + [VIS_MODE_NORMAL] = { + .parent = &vis[VIS_MODE_MOVE], + .bindings = vis_normal, + }, + [VIS_MODE_VISUAL] = { + .name = "VISUAL", + .parent = &vis[VIS_MODE_MOVE], + .bindings = vis_visual, + .enter = vis_visual_enter, + .leave = vis_visual_leave, + }, + [VIS_MODE_INSERT] = { + .name = "INSERT", + .parent = &vis[VIS_MODE_BASIC], + .bindings = vis_insert, + .input = vis_insert_input, + }, + [VIS_MODE_REPLACE] = { + .name = "REPLACE", + .parent = &vis[VIS_MODE_BASIC], + .bindings = vis_replace, + .input = vis_replace_input, + }, +}; + +static void switchmode(const Arg *arg) { + if (mode->leave) + mode->leave(); + mode = &vis[arg->i]; + if (mode->enter) + mode->enter(); + // TODO display mode name somewhere? +} + +/* incomplete list of usefule but currently missing functionality from nanos help ^G: + +^X (F2) Close the current file buffer / Exit from nano +^O (F3) Write the current file to disk +^J (F4) Justify the current paragraph + +^R (F5) Insert another file into the current one +^W (F6) Search for a string or a regular expression + +^K (F9) Cut the current line and store it in the cutbuffer +^U (F10) Uncut from the cutbuffer into the current line +^T (F12) Invoke the spell checker, if available + + +^_ (F13) (M-G) Go to line and column number +^\ (F14) (M-R) Replace a string or a regular expression +^^ (F15) (M-A) Mark text at the cursor position +M-W (F16) Repeat last search + +M-^ (M-6) Copy the current line and store it in the cutbuffer +M-V Insert the next keystroke verbatim + +XXX: CONTROL(' ') = 0, ^Space Go forward one word +*/ + +/* key binding configuration */ +#if 1 +static KeyBinding nano_keys[] = { + { { CONTROL('D') }, cursor, { .m = editor_delete } }, + BACKSPACE( cursor, editor_backspace ), + { { KEY(LEFT) }, cursor, { .m = editor_char_prev } }, + { { KEY(RIGHT) }, cursor, { .m = editor_char_next } }, + { { CONTROL('F') }, cursor, { .m = editor_char_next } }, + { { KEY(UP) }, cursor, { .m = editor_line_up } }, + { { CONTROL('P') }, cursor, { .m = editor_line_up } }, + { { KEY(DOWN) }, cursor, { .m = editor_line_down } }, + { { CONTROL('N') }, cursor, { .m = editor_line_down } }, + { { KEY(PPAGE) }, cursor, { .m = editor_page_up } }, + { { CONTROL('Y') }, cursor, { .m = editor_page_up } }, + { { KEY(F(7)) }, cursor, { .m = editor_page_up } }, + { { KEY(NPAGE) }, cursor, { .m = editor_page_down } }, + { { CONTROL('V') }, cursor, { .m = editor_page_down } }, + { { KEY(F(8)) }, cursor, { .m = editor_page_down } }, +// { { CONTROL(' ') }, cursor, { .m = editor_word_start_next } }, + { { META(' ') }, cursor, { .m = editor_word_start_prev } }, + { { CONTROL('A') }, cursor, { .m = editor_line_start } }, + { { CONTROL('E') }, cursor, { .m = editor_line_end } }, + { { META(']') }, cursor, { .m = editor_bracket_match } }, + { { META(')') }, cursor, { .m = editor_paragraph_next } }, + { { META('(') }, cursor, { .m = editor_paragraph_prev } }, + { { META('\\') }, cursor, { .m = editor_file_begin } }, + { { META('|') }, cursor, { .m = editor_file_begin } }, + { { META('/') }, cursor, { .m = editor_file_end } }, + { { META('?') }, cursor, { .m = editor_file_end } }, + { { META('U') }, call, { .f = editor_undo } }, + { { META('E') }, call, { .f = editor_redo } }, + { { CONTROL('I') }, insert, { .s = "\t" } }, + /* TODO: handle this in editor to insert \n\r when appriopriate */ + { { CONTROL('M') }, insert, { .s = "\n" } }, + { { CONTROL('L') }, call, { .f = editor_draw } }, + { /* empty last element, array terminator */ }, +}; +#endif + +static Mode nano[] = { + { .parent = NULL, .bindings = movement, }, + { .parent = &nano[0], .bindings = nano_keys, .input = vis_insert_input, }, +}; + +/* list of editor configurations, first entry is default. name is matched with + * argv[0] i.e. program name upon execution + */ +static Config editors[] = { + { .name = "vis", .mode = &vis[VIS_MODE_NORMAL] }, + { .name = "nano", .mode = &nano[1] }, +}; + +/* Color definitions, by default the i-th color is used for the i-th syntax + * rule below. A color value of -1 specifies the default terminal color. + */ +static Color colors[] = { + { .fg = COLOR_RED, .bg = -1, .attr = A_BOLD }, + { .fg = COLOR_GREEN, .bg = -1, .attr = A_BOLD }, + { .fg = COLOR_GREEN, .bg = -1, .attr = A_NORMAL }, + { .fg = COLOR_MAGENTA, .bg = -1, .attr = A_BOLD }, + { .fg = COLOR_MAGENTA, .bg = -1, .attr = A_NORMAL }, + { .fg = COLOR_BLUE, .bg = -1, .attr = A_BOLD }, + { .fg = COLOR_RED, .bg = -1, .attr = A_NORMAL }, + { .fg = COLOR_BLUE, .bg = -1, .attr = A_NORMAL }, + { .fg = COLOR_BLUE, .bg = -1, .attr = A_NORMAL }, + { /* empty last element, array terminator */ } +}; + +/* Syntax color definition, you can define up to SYNTAX_REGEX_RULES + * number of regex rules per file type. Each rule is requires a regular + * expression and corresponding compilation flags. Optionally a color in + * the form + * + * { .fg = COLOR_YELLOW, .bg = -1, .attr = A_BOLD } + * + * can be specified. If such a color definition is missing the i-th element + * of the colors array above is used instead. + * + * The array of syntax definition must be terminated with an empty element. + */ +#define B "\\b" +/* Use this if \b is not in your libc's regex implementation */ +// #define B "^| |\t|\\(|\\)|\\[|\\]|\\{|\\}|\\||$ + +// changes wrt sandy #precoressor: # idfdef, #include <file.h> between brackets +static Syntax syntaxes[] = {{ + .name = "c", + .file = "\\.(c(pp|xx)?|h(pp|xx)?|cc)$", + .rules = {{ + "$^", + REG_NEWLINE, + },{ + B"(for|if|while|do|else|case|default|switch|try|throw|catch|operator|new|delete)"B, + REG_NEWLINE, + },{ + B"(float|double|bool|char|int|short|long|sizeof|enum|void|static|const|struct|union|" + "typedef|extern|(un)?signed|inline|((s?size)|((u_?)?int(8|16|32|64|ptr)))_t|class|" + "namespace|template|public|protected|private|typename|this|friend|virtual|using|" + "mutable|volatile|register|explicit)"B, + REG_NEWLINE, + },{ + B"(goto|continue|break|return)"B, + REG_NEWLINE, + },{ + "(^#[\\t ]*(define|include(_next)?|(un|ifn?)def|endif|el(if|se)|if|warning|error|pragma))|" + B"[A-Z_][0-9A-Z_]+"B"", + REG_NEWLINE, + },{ + "(\\(|\\)|\\{|\\}|\\[|\\])", + REG_NEWLINE, + },{ + "(\"(\\\\.|[^\"])*\")", + //"([\"<](\\\\.|[^ \">])*[\">])", + REG_NEWLINE, + },{ + "(//.*)", + REG_NEWLINE, + },{ + "(/\\*([^*]|\\*[^/])*\\*/|/\\*([^*]|\\*[^/])*$|^([^/]|/[^*])*\\*/)", + }}, +},{ + /* empty last element, array terminator */ +}}; diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..c570914 --- /dev/null +++ b/config.mk @@ -0,0 +1,17 @@ +# vis version +VERSION = devel + +# Customize below to fit your system + +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man + +INCS = -I. +LIBS = -lc -lncursesw + +CFLAGS += -std=c99 -Os ${INCS} -DVERSION=\"${VERSION}\" -DNDEBUG +LDFLAGS += ${LIBS} + +DEBUG_CFLAGS = ${CFLAGS} -UNDEBUG -O0 -g -ggdb -Wall -Wextra -Wno-missing-field-initializers -Wno-unused-parameter + +CC = cc diff --git a/editor.c b/editor.c new file mode 100644 index 0000000..009511d --- /dev/null +++ b/editor.c @@ -0,0 +1,1305 @@ +/* + * Copyright (c) 2014 Marc André Tanner <mat at brain-dump.org> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#define _XOPEN_SOURCE +#include <string.h> +#include <stdlib.h> +#include <wchar.h> +#include <ctype.h> +#include <errno.h> +#include "editor.h" +#include "text.h" +#include "util.h" + +typedef struct { /* used to calculate the display width of a character */ + char c[7]; /* utf8 encoded bytes */ + size_t len; /* number of bytes of the multibyte sequence */ + wchar_t wchar; /* equivalent converted wide character, needed for wcwidth(3) */ +} Char; + +typedef struct { + unsigned char len; /* number of bytes the character displayed in this cell uses, for + character which use more than 1 column to display, their lenght + is stored in the leftmost cell wheras all following cells + occupied by the same character have a length of 0. */ + char data; /* first byte of the utf8 sequence, used for white space handling */ +} Cell; + +typedef struct Line Line; +struct Line { /* a line on the screen, *not* in the file */ + Line *prev, *next; /* pointer to neighbouring screen lines */ + size_t len; /* line length in terms of bytes */ + size_t lineno; /* line number from start of file */ + int width; /* zero based position of last used column cell */ + Cell cells[]; /* win->width cells storing information about the displayed characters */ +}; + +typedef struct { /* cursor position */ + Filepos pos; /* in bytes from the start of the file */ + int row, col; /* in terms of zero based screen coordinates */ + Line *line; /* screen line on which cursor currently resides */ +} Cursor; + +typedef struct Win Win; +struct Win { /* window showing part of a file */ + Editor *editor; + Text *text; /* underlying text management */ + WINDOW *win; /* curses window for the text area */ + WINDOW *statuswin; /* curses window for the statusbar */ + int width, height; /* text area size *not* including the statusbar */ + Filepos start, end; /* currently displayed area [start, end] in bytes from the start of the file */ + Line *lines; /* win->height number of lines representing window content */ + Line *topline; /* top of the window, first line currently shown */ + Line *lastline; /* last currently used line, always <= bottomline */ + Line *bottomline; /* bottom of screen, might be unused if lastline < bottomline */ + Filerange sel; /* selected text range in bytes from start of file */ + Cursor cursor; /* current window cursor position */ + + Line *line; // TODO: rename to something more descriptive, these are the current drawing pos + int col; + + Syntax *syntax; /* syntax highlighting definitions for this window or NULL */ + int tabwidth; /* how many spaces should be used to display a tab character */ + Win *prev, *next; /* neighbouring windows */ +}; + +struct Editor { + int width, height; /* terminal size, available for all windows */ + editor_statusbar_t statusbar; + Win *windows; /* list of windows */ + Win *win; /* currently active window */ + Syntax *syntaxes; /* NULL terminated array of syntax definitions */ + void (*windows_arrange)(Editor*); +}; + +static void editor_windows_arrange(Editor *ed); +static void editor_windows_arrange_horizontal(Editor *ed); +static void editor_windows_arrange_vertical(Editor *ed); +static Filerange window_selection_get(Win *win); +static void window_statusbar_draw(Win *win); +static void editor_windows_invalidate(Editor *ed, size_t pos, size_t len); +static void editor_windows_insert(Editor *ed, size_t pos, const char *c, size_t len); +static void editor_windows_delete(Editor *ed, size_t pos, size_t len); +static void editor_search_forward(Editor *ed, Regex *regex); +static void editor_search_backward(Editor *ed, Regex *regex); +static void window_selection_clear(Win *win); +static void window_clear(Win *win); +static bool window_addch(Win *win, Char *c); +static size_t window_cursor_update(Win *win); +static size_t window_line_begin(Win *win); +static size_t cursor_move_to(Win *win, size_t pos); +static size_t cursor_move_to_line(Win *win, size_t pos); +static void window_draw(Win *win); +static bool window_resize(Win *win, int width, int height); +static void window_move(Win *win, int x, int y); +static void window_free(Win *win); +static Win *window_new(Editor *ed, const char *filename, int x, int y, int w, int h, int pos); +static size_t pos_by_line(Win *win, Line *line); +static size_t cursor_offset(Cursor *cursor); +static bool scroll_line_down(Win *win, int n); +static bool scroll_line_up(Win *win, int n); + +static void window_selection_clear(Win *win) { + win->sel.start = win->sel.end = (size_t)-1; + window_draw(win); + window_cursor_update(win); +} + +static void window_clear(Win *win) { + size_t line_size = sizeof(Line) + win->width*sizeof(Cell); + win->topline = win->lines; + win->topline->lineno = text_lineno_by_pos(win->text, win->start); + win->lastline = win->topline; + Line *prev = NULL; + for (int i = 0; i < win->height; i++) { + Line *line = (Line*)(((char*)win->lines) + i*line_size); + line->len = 0; + line->width = 0; + line->prev = prev; + if (prev) + prev->next = line; + prev = line; + } + win->bottomline = prev; + win->line = win->topline; + win->col = 0; +} + +static Filerange window_selection_get(Win *win) { + Filerange sel = win->sel; + if (sel.start == (size_t)-1) { + sel.end = (size_t)-1; + } else if (sel.end == (size_t)-1) { + sel.start = (size_t)-1; + } else if (sel.start > sel.end) { + size_t tmp = sel.start; + sel.start = sel.end; + sel.end = tmp; + } + return sel; +} + +/* try to add another character to the window, return whether there was space left */ +static bool window_addch(Win *win, Char *c) { + if (!win->line) + return false; + + Cell empty = {}; + int width; + size_t lineno = win->line->lineno; + + switch (c->wchar) { + case '\t': + width = win->tabwidth - (win->col % win->tabwidth); + for (int w = 0; w < width; w++) { + if (win->col + 1 > win->width) { + win->line = win->line->next; + win->col = 0; + if (!win->line) + return false; + win->line->lineno = lineno; + } + if (w == 0) { + /* first cell of a tab has a length of 1 */ + win->line->cells[win->col].len = c->len; + win->line->len += c->len; + } else { + /* all remaining ones have a lenght of zero */ + win->line->cells[win->col].len = 0; + } + /* but all are marked as part of a tabstop */ + win->line->cells[win->col].data = '\t'; + win->col++; + win->line->width++; + waddch(win->win, ' '); + } + return true; + case '\n': + width = 1; + if (win->col + width > win->width) { + win->line = win->line->next; + win->col = 0; + if (!win->line) + return false; + win->line->lineno = lineno + 1; + } + win->line->cells[win->col].len = c->len; + win->line->len += c->len; + win->line->cells[win->col].data = '\n'; + for (int i = win->col + 1; i < win->width; i++) + win->line->cells[i] = empty; + + if (win->line == win->bottomline) { + /* XXX: curses bug? the wclrtoeol(win->win); implied by waddch(win->win, '\n') + * doesn't seem to work on the last line!? + * + * Thus explicitly clear the remaining of the line. + */ + for (int i = win->col; i < win->width; i++) + waddch(win->win, ' '); + } else if (win->line->width == 0) { + /* add a single space in an otherwise empty line, makes selection cohorent */ + waddch(win->win, ' '); + } + + waddch(win->win, '\n'); + win->line = win->line->next; + if (win->line) + win->line->lineno = lineno + 1; + win->col = 0; + return true; + default: + if (c->wchar < 128 && !isprint(c->wchar)) { + /* non-printable ascii char, represent it as ^(char + 64) */ + Char s = { .c = "^_", .len = 1 }; + s.c[1] = c->c[0] + 64; + *c = s; + width = 2; + } else { + if ((width = wcwidth(c->wchar)) == -1) { + /* this should never happen */ + width = 1; + } + } + + if (win->col + width > win->width) { + for (int i = win->col; i < win->width; i++) + win->line->cells[i] = empty; + win->line = win->line->next; + win->col = 0; + } + + if (win->line) { + win->line->width += width; + win->line->len += c->len; + win->line->lineno = lineno; + win->line->cells[win->col].len = c->len; + win->line->cells[win->col].data = c->c[0]; + win->col++; + /* set cells of a character which uses multiple columns */ + for (int i = 1; i < width; i++) + win->line->cells[win->col++] = empty; + waddstr(win->win, c->c); + return true; + } + return false; + } +} + +static void window_statusbar_draw(Win *win) { + if (!win->editor->statusbar) + return; + Cursor *cursor = &win->cursor; + Line *line = cursor->line; + size_t lineno = line->lineno; + int col = cursor->col; + while (line->prev && line->prev->lineno == lineno) { + line = line->prev; + col += line->width; + } + win->editor->statusbar(win->statuswin, win->editor->win == win, text_filename(win->text), cursor->line->lineno, col+1); +} + +/* place the cursor according to the screen coordinates in win->{row,col} and + * update the statusbar. if a selection is active, redraw the window to reflect + * its changes. */ +static size_t window_cursor_update(Win *win) { + Cursor *cursor = &win->cursor; + if (win->sel.start != (size_t)-1) { + win->sel.end = cursor->pos; + window_draw(win); + } + wmove(win->win, cursor->row, cursor->col); + window_statusbar_draw(win); + return cursor->pos; +} + +/* move the cursor to the character at pos bytes from the begining of the file. + * if pos is not in the current viewport, redraw the window to make it visible */ +static size_t cursor_move_to(Win *win, size_t pos) { + Line *line = win->topline; + int row = 0; + int col = 0; + size_t max = text_size(win->text); + + if (pos > max) + pos = max > 0 ? max - 1 : 0; + + if (pos < win->start || pos > win->end) { + win->start = pos; + window_draw(win); + } else { + size_t cur = win->start; + while (line && line != win->lastline && cur < pos) { + if (cur + line->len > pos) + break; + cur += line->len; + line = line->next; + row++; + } + + if (line) { + int max_col = MIN(win->width, line->width); + while (cur < pos && col < max_col) { + cur += line->cells[col].len; + col++; + } + while (col < max_col && line->cells[col].data == '\t') + col++; + } else { + line = win->bottomline; + row = win->height - 1; + } + } + + win->cursor.line = line; + win->cursor.row = row; + win->cursor.col = col; + win->cursor.pos = pos; + return window_cursor_update(win); +} + +/* move cursor to pos, make sure the whole line is visible */ +static size_t cursor_move_to_line(Win *win, size_t pos) { + if (pos < win->start || pos > win->end) { + win->cursor.pos = pos; + window_line_begin(win); + } + return cursor_move_to(win, pos); +} + +/* redraw the complete with data starting from win->start bytes into the file. + * stop once the screen is full, update win->end, win->lastline */ +static void window_draw(Win *win) { + window_clear(win); + wmove(win->win, 0, 0); + /* current absolute file position */ + size_t pos = win->start; + /* number of bytes to read in one go */ + // TODO read smaller junks + size_t text_len = win->width * win->height; + /* current buffer to work with */ + char text[text_len+1]; + /* remaining bytes to process in buffer*/ + size_t rem = text_bytes_get(win->text, pos, text_len, text); + /* NUL terminate because regex(3) function expect it */ + text[rem] = '\0'; + /* current position into buffer from which to interpret a character */ + char *cur = text; + /* current 'parsed' character' */ + Char c; + /* current selection */ + Filerange sel = window_selection_get(win); + /* matched tokens for each syntax rule */ + regmatch_t match[SYNTAX_REGEX_RULES][1]; + if (win->syntax) { + for (int i = 0; i < LENGTH(win->syntax->rules); i++) { + SyntaxRule *rule = &win->syntax->rules[i]; + if (!rule->rule) + break; + if (regexec(&rule->regex, cur, 1, match[i], 0) || + match[i][0].rm_so == match[i][0].rm_eo) { + match[i][0].rm_so = -1; + match[i][0].rm_eo = -1; + } + } + } + + while (rem > 0) { + + int attrs = COLOR_PAIR(0) | A_NORMAL; + + if (win->syntax) { + size_t off = cur - text; /* number of already processed bytes */ + for (int i = 0; i < LENGTH(win->syntax->rules); i++) { + SyntaxRule *rule = &win->syntax->rules[i]; + if (!rule->rule) + break; + if (match[i][0].rm_so == -1) + continue; /* no match on whole text */ + if (off >= (size_t)match[i][0].rm_eo) { + /* past match, continue search from current position */ + if (regexec(&rule->regex, cur, 1, match[i], 0) || + match[i][0].rm_so == match[i][0].rm_eo) { + match[i][0].rm_so = -1; + match[i][0].rm_eo = -1; + continue; + } + match[i][0].rm_so += off; + match[i][0].rm_eo += off; + } + + if (text + match[i][0].rm_so <= cur && cur < text + match[i][0].rm_eo) { + /* within matched expression */ + attrs = rule->color.attr; + } + } + } + + if (sel.start <= pos && pos < sel.end) + attrs |= A_REVERSE; // TODO: make configurable + + size_t len = mbrtowc(&c.wchar, cur, rem, NULL); + if (len == (size_t)-1 && errno == EILSEQ) { + /* ok, we encountered an invalid multibyte sequence, + * replace it with the Unicode Replacement Character + * (FFFD) and skip until the start of the next utf8 char */ + for (len = 1; rem > len && !isutf8(cur[len]); len++); + c = (Char){ .c = "\xEF\xBF\xBD", .wchar = 0xFFFD, .len = len }; + } else if (len == (size_t)-2) { + /* not enough bytes available to convert to a + * wide character. advance file position and read + * another junk into buffer. + */ + rem = text_bytes_get(win->text, pos, text_len, text); + text[rem] = '\0'; + cur = text; + continue; + } else if (len == 0) { + /* NUL byte encountered, store it and continue */ + len = 1; + c = (Char){ .c = "\x00", .wchar = 0x00, .len = len }; + } else { + for (size_t i = 0; i < len; i++) + c.c[i] = cur[i]; + c.c[len] = '\0'; + c.len = len; + } + + if (cur[0] == '\n' && rem > 1 && cur[1] == '\r') { + /* convert windows style newline \n\r into a single char with len = 2 */ + c.len = len = 2; + } + + wattrset(win->win, attrs); + if (!window_addch(win, &c)) + break; + + rem -= len; + cur += len; + pos += len; + } + + /* set end of viewing region */ + win->end = pos; + win->lastline = win->line ? win->line : win->bottomline; + win->lastline->next = NULL; + /* and clear the rest of the unused window */ + wclrtobot(win->win); +} + +static bool window_resize(Win *win, int width, int height) { + height--; // statusbar + if (wresize(win->win, height, width) == ERR || + wresize(win->statuswin, 1, width) == ERR) + return false; + + // TODO: only grow memory area + win->height = height; + win->width = width; + free(win->lines); + size_t line_size = sizeof(Line) + width*sizeof(Cell); + if (!(win->lines = calloc(height, line_size))) + return false; + window_draw(win); + cursor_move_to(win, win->cursor.pos); + return true; +} + +static void window_move(Win *win, int x, int y) { + mvwin(win->win, y, x); + mvwin(win->statuswin, y + win->height, x); +} + +static void window_free(Win *win) { + if (!win) + return; + if (win->win) + delwin(win->win); + if (win->statuswin) + delwin(win->statuswin); + text_free(win->text); + free(win); +} + +static Win *window_new(Editor *ed, const char *filename, int x, int y, int w, int h, int pos) { + Win *win = calloc(1, sizeof(Win)); + if (!win) + return NULL; + win->editor = ed; + win->start = pos; + win->tabwidth = 8; // TODO make configurable + + if (filename) { + for (Syntax *syn = ed->syntaxes; syn && syn->name; syn++) { + if (!regexec(&syn->file_regex, filename, 0, NULL, 0)) { + win->syntax = syn; + break; + } + } + } + + if (!(win->text = text_load(filename)) || + !(win->win = newwin(h-1, w, y, x)) || + !(win->statuswin = newwin(1, w, y+h-1, x)) || + !window_resize(win, w, h)) { + window_free(win); + return NULL; + } + + window_selection_clear(win); + cursor_move_to(win, pos); + + return win; +} + +Editor *editor_new(int width, int height, editor_statusbar_t statusbar) { + Editor *ed = calloc(1, sizeof(Editor)); + if (!ed) + return NULL; + ed->width = width; + ed->height = height; + ed->statusbar = statusbar; + ed->windows_arrange = editor_windows_arrange_horizontal; + return ed; +} + +bool editor_load(Editor *ed, const char *filename) { + Win *win = window_new(ed, filename, 0, 0, ed->width, ed->height, 0); + if (!win) + return false; + + if (ed->windows) + ed->windows->prev = win; + win->next = ed->windows; + win->prev = NULL; + ed->windows = win; + ed->win = win; + return true; +} + +static size_t pos_by_line(Win *win, Line *line) { + size_t pos = win->start; + for (Line *cur = win->topline; cur && cur != line; cur = cur->next) + pos += cur->len; + return pos; +} + +size_t editor_char_prev(Editor *ed) { + Win *win = ed->win; + Cursor *cursor = &win->cursor; + Line *line = cursor->line; + + do { + if (cursor->col == 0) { + if (!line->prev) + return cursor->pos; + cursor->line = line = line->prev; + cursor->col = MIN(line->width, win->width - 1); + cursor->row--; + } else { + cursor->col--; + } + } while (line->cells[cursor->col].len == 0); + + cursor->pos -= line->cells[cursor->col].len; + return window_cursor_update(win); +} + +size_t editor_char_next(Editor *ed) { + Win *win = ed->win; + Cursor *cursor = &win->cursor; + Line *line = cursor->line; + + do { + cursor->pos += line->cells[cursor->col].len; + if ((line->width == win->width && cursor->col == win->width - 1) || + cursor->col == line->width) { + if (!line->next) + return cursor->pos; + cursor->line = line = line->next; + cursor->row++; + cursor->col = 0; + } else { + cursor->col++; + } + } while (line->cells[cursor->col].len == 0); + + return window_cursor_update(win); +} + +/* calculate the line offset in bytes of a given cursor position, used after + * the cursor changes line. this assumes cursor->line already points to the new + * line, but cursor->col still has the old column position of the previous + * line based on which the new column position is calculated */ +static size_t cursor_offset(Cursor *cursor) { + Line *line = cursor->line; + int col = cursor->col; + int off = 0; + /* for characters which use more than 1 column, make sure we are on the left most */ + while (col > 0 && line->cells[col].len == 0 && line->cells[col].data != '\t') + col--; + while (col < line->width && line->cells[col].data == '\t') + col++; + for (int i = 0; i < col; i++) + off += line->cells[i].len; + cursor->col = col; + return off; +} + +static bool scroll_line_down(Win *win, int n) { + Line *line; + if (win->end == text_size(win->text)) + return false; + for (line = win->topline; line && n > 0; line = line->next, n--) + win->start += line->len; + window_draw(win); + /* try to place the cursor at the same column */ + Cursor *cursor = &win->cursor; + cursor->pos = win->end - win->lastline->len + cursor_offset(cursor); + window_cursor_update(win); + return true; +} + +static bool scroll_line_up(Win *win, int n) { + /* scrolling up is somewhat tricky because we do not yet know where + * the lines start, therefore scan backwards but stop at a reasonable + * maximum in case we are dealing with a file without any newlines + */ + if (win->start == 0) + return false; + size_t max = win->width * win->height / 2; + char c; + Iterator it = text_iterator_get(win->text, win->start - 1); + + if (!text_iterator_byte_get(&it, &c)) + return false; + size_t off = 0; + /* skip newlines immediately before display area */ + if (c == '\r' && text_iterator_byte_prev(&it, &c)) + off++; + if (c == '\n' && text_iterator_byte_prev(&it, &c)) + off++; + do { + if ((c == '\n' || c == '\r') && --n == 0) + break; + if (++off > max) + break; + } while (text_iterator_byte_prev(&it, &c)); + win->start -= off; + window_draw(win); + Cursor *cursor = &win->cursor; + cursor->pos = win->start + cursor_offset(cursor); + window_cursor_update(win); + return true; +} + +size_t editor_file_begin(Editor *ed) { + return cursor_move_to(ed->win, 0); +} + +size_t editor_file_end(Editor *ed) { + Win *win = ed->win; + size_t size = text_size(win->text); + if (win->end == size) + return cursor_move_to(win, win->end); + win->start = size - 1; + scroll_line_up(win, win->height / 2); + return cursor_move_to(win, win->end); +} + +size_t editor_page_up(Editor *ed) { + Win *win = ed->win; + if (!scroll_line_up(win, win->height)) + cursor_move_to(win, win->start); + return win->cursor.pos; +} + +size_t editor_page_down(Editor *ed) { + Win *win = ed->win; + Cursor *cursor = &win->cursor; + if (win->end == text_size(win->text)) + return cursor_move_to(win, win->end); + win->start = win->end; + window_draw(win); + int col = cursor->col; + cursor_move_to(win, win->start); + cursor->col = col; + cursor->pos += cursor_offset(cursor); + return window_cursor_update(win); +} + +size_t editor_line_up(Editor *ed) { + Win *win = ed->win; + Cursor *cursor = &win->cursor; + if (!cursor->line->prev) { + scroll_line_up(win, 1); + return cursor->pos; + } + cursor->row--; + cursor->line = cursor->line->prev; + cursor->pos = pos_by_line(win, cursor->line) + cursor_offset(cursor); + return window_cursor_update(win); +} + +size_t editor_line_down(Editor *ed) { + Win *win = ed->win; + Cursor *cursor = &win->cursor; + if (!cursor->line->next) { + if (cursor->line == win->bottomline) + scroll_line_down(ed->win, 1); + return cursor->pos; + } + cursor->row++; + cursor->line = cursor->line->next; + cursor->pos = pos_by_line(win, cursor->line) + cursor_offset(cursor); + return window_cursor_update(win); +} + +static size_t window_line_begin(Win *win) { + char c; + size_t pos = win->cursor.pos; + Iterator it = text_iterator_get(win->text, pos); + if (!text_iterator_byte_get(&it, &c)) + return pos; + if (c == '\r') + text_iterator_byte_prev(&it, &c); + if (c == '\n') + text_iterator_byte_prev(&it, &c); + while (text_iterator_byte_get(&it, &c)) { + if (c == '\n' || c == '\r') { + it.pos++; + break; + } + text_iterator_byte_prev(&it, NULL); + } + return cursor_move_to(win, it.pos); +} + +size_t editor_line_begin(Editor *ed) { + return window_line_begin(ed->win); +} + +size_t editor_line_start(Editor *ed) { + char c; + Win *win = ed->win; + editor_line_begin(ed); + Iterator it = text_iterator_get(win->text, win->cursor.pos); + while (text_iterator_byte_get(&it, &c) && c != '\n' && isspace(c)) + text_iterator_byte_next(&it, NULL); + while (win->end < it.pos && scroll_line_down(win, 1)); + return cursor_move_to(win, it.pos); +} + +size_t editor_line_end(Editor *ed) { + char c; + Win *win = ed->win; + Iterator it = text_iterator_get(win->text, win->cursor.pos); + while (text_iterator_byte_get(&it, &c) && c != '\n') + text_iterator_byte_next(&it, NULL); + while (win->end < it.pos && scroll_line_down(win, 1)); + return cursor_move_to(win, it.pos); +} + +size_t editor_line_finish(Editor *ed) { + char c; + Win *win = ed->win; + Iterator it = text_iterator_get(win->text, editor_line_end(ed)); + do text_iterator_byte_prev(&it, NULL); + while (text_iterator_byte_get(&it, &c) && c != '\n' && c != '\r' && isspace(c)); + return cursor_move_to(win, it.pos); +} + +size_t editor_word_end_prev(Editor *ed) { + char c; + Win *win = ed->win; + Iterator it = text_iterator_get(win->text, win->cursor.pos); + while (text_iterator_byte_prev(&it, &c) && !isspace(c)); + while (text_iterator_char_prev(&it, &c) && isspace(c)); + while (win->start > it.pos && scroll_line_up(win, 1)); + return cursor_move_to(win, it.pos); +} + +size_t editor_word_end_next(Editor *ed) { + char c; + Win *win = ed->win; + size_t pos = win->cursor.pos; + Iterator it = text_iterator_get(win->text, pos); + while (text_iterator_char_next(&it, &c) && isspace(c)); + do pos = it.pos; while (text_iterator_char_next(&it, &c) && !isspace(c)); + while (win->end < pos && scroll_line_down(win, 1)); + return cursor_move_to(win, pos); +} + +size_t editor_word_start_next(Editor *ed) { + char c; + Win *win = ed->win; + Iterator it = text_iterator_get(win->text, win->cursor.pos); + while (text_iterator_byte_get(&it, &c) && !isspace(c)) + text_iterator_byte_next(&it, NULL); + while (text_iterator_byte_get(&it, &c) && isspace(c)) + text_iterator_byte_next(&it, NULL); + while (win->end < it.pos && scroll_line_down(win, 1)); + return cursor_move_to(win, it.pos); +} + +size_t editor_word_start_prev(Editor *ed) { + char c; + Win *win = ed->win; + size_t pos = win->cursor.pos; + Iterator it = text_iterator_get(win->text, pos); + while (text_iterator_byte_prev(&it, &c) && isspace(c)); + do pos = it.pos; while (text_iterator_char_prev(&it, &c) && !isspace(c)); + while (win->start > pos && scroll_line_up(win, 1)); + return cursor_move_to(win, pos); +} + +static size_t editor_paragraph_sentence_next(Editor *ed, bool sentence) { + char c; + bool content = false, paragraph = false; + Win *win = ed->win; + size_t pos = win->cursor.pos; + Iterator it = text_iterator_get(win->text, pos); + while (text_iterator_byte_next(&it, &c)) { + content |= !isspace(c); + if (sentence && (c == '.' || c == '?' || c == '!') && text_iterator_byte_next(&it, &c) && isspace(c)) { + if (c == '\n' && text_iterator_byte_next(&it, &c)) { + if (c == '\r') + text_iterator_byte_next(&it, &c); + } else { + while (text_iterator_byte_get(&it, &c) && c == ' ') + text_iterator_byte_next(&it, NULL); + } + break; + } + if (c == '\n' && text_iterator_byte_next(&it, &c)) { + if (c == '\r') + text_iterator_byte_next(&it, &c); + content |= !isspace(c); + if (c == '\n') + paragraph = true; + } + if (content && paragraph) + break; + } + while (win->end < it.pos && scroll_line_down(win, 1)); + return cursor_move_to(win, it.pos); +} + +size_t editor_sentence_next(Editor *ed) { + return editor_paragraph_sentence_next(ed, true); +} + +size_t editor_paragraph_next(Editor *ed) { + return editor_paragraph_sentence_next(ed, false); +} + +static size_t editor_paragraph_sentence_prev(Editor *ed, bool sentence) { + char prev, c; + bool content = false, paragraph = false; + Win *win = ed->win; + size_t pos = win->cursor.pos; + + Iterator it = text_iterator_get(win->text, pos); + if (!text_iterator_byte_get(&it, &prev)) + return pos; + + while (text_iterator_byte_prev(&it, &c)) { + content |= !isspace(c) && c != '.' && c != '?' && c != '!'; + if (sentence && content && (c == '.' || c == '?' || c == '!') && isspace(prev)) { + do text_iterator_byte_next(&it, NULL); + while (text_iterator_byte_get(&it, &c) && isspace(c)); + break; + } + if (c == '\r') + text_iterator_byte_prev(&it, &c); + if (c == '\n' && text_iterator_byte_prev(&it, &c)) { + content |= !isspace(c); + if (c == '\r') + text_iterator_byte_prev(&it, &c); + if (c == '\n') { + paragraph = true; + if (content) { + do text_iterator_byte_next(&it, NULL); + while (text_iterator_byte_get(&it, &c) && isspace(c)); + break; + } + } + } + if (content && paragraph) { + do text_iterator_byte_next(&it, NULL); + while (text_iterator_byte_get(&it, &c) && !isspace(c)); + break; + } + prev = c; + } + while (win->end < it.pos && scroll_line_down(win, 1)); + return cursor_move_to(win, it.pos); +} + +size_t editor_sentence_prev(Editor *ed) { + return editor_paragraph_sentence_prev(ed, true); +} + +size_t editor_paragraph_prev(Editor *ed) { + return editor_paragraph_sentence_prev(ed, false); +} + +size_t editor_bracket_match(Editor *ed) { + Win *win = ed->win; + int direction, count = 1; + char search, current, c; + size_t pos = win->cursor.pos; + Iterator it = text_iterator_get(win->text, pos); + if (!text_iterator_byte_get(&it, ¤t)) + return pos; + + switch (current) { + case '(': search = ')'; direction = 1; break; + case ')': search = '('; direction = -1; break; + case '{': search = '}'; direction = 1; break; + case '}': search = '{'; direction = -1; break; + case '[': search = ']'; direction = 1; break; + case ']': search = '['; direction = -1; break; + case '<': search = '>'; direction = 1; break; + case '>': search = '<'; direction = -1; break; + case '"': search = '"'; direction = 1; break; + default: return pos; + } + + if (direction >= 0) { /* forward search */ + while (text_iterator_byte_next(&it, &c)) { + if (c == search && --count == 0) { + pos = it.pos; + break; + } else if (c == current) { + count++; + } + } + } else { /* backwards */ + while (text_iterator_byte_prev(&it, &c)) { + if (c == search && --count == 0) { + pos = it.pos; + break; + } else if (c == current) { + count++; + } + } + } + + return cursor_move_to_line(win, pos); +} + +void editor_draw(Editor *ed) { + for (Win *win = ed->windows; win; win = win->next) { + if (ed->win != win) { + window_draw(win); + cursor_move_to(win, win->cursor.pos); + } + } + window_draw(ed->win); + cursor_move_to(ed->win, ed->win->cursor.pos); +} + +void editor_update(Editor *ed) { + for (Win *win = ed->windows; win; win = win->next) { + if (ed->win != win) { + wnoutrefresh(win->statuswin); + wnoutrefresh(win->win); + } + } + + wnoutrefresh(ed->win->statuswin); + wnoutrefresh(ed->win->win); +} + +void editor_free(Editor *ed) { + if (!ed) + return; + window_free(ed->win); + free(ed); +} + +bool editor_resize(Editor *ed, int width, int height) { + ed->width = width; + ed->height = height; + editor_windows_arrange(ed); + return true; +} + +bool editor_syntax_load(Editor *ed, Syntax *syntaxes, Color *colors) { + bool success = true; + ed->syntaxes = syntaxes; + for (Syntax *syn = syntaxes; syn && syn->name; syn++) { + if (regcomp(&syn->file_regex, syn->file, REG_EXTENDED|REG_NOSUB|REG_ICASE|REG_NEWLINE)) + success = false; + Color *color = colors; + for (int j = 0; j < LENGTH(syn->rules); j++) { + SyntaxRule *rule = &syn->rules[j]; + if (!rule->rule) + break; + if (rule->color.fg == 0 && color && color->fg != 0) + rule->color = *color++; + if (rule->color.attr == 0) + rule->color.attr = A_NORMAL; + if (rule->color.fg != 0) + rule->color.attr |= COLOR_PAIR(editor_color_reserve(rule->color.fg, rule->color.bg)); + if (regcomp(&rule->regex, rule->rule, REG_EXTENDED|rule->cflags)) + success = false; + } + } + + return success; +} + +void editor_syntax_unload(Editor *ed) { + for (Syntax *syn = ed->syntaxes; syn && syn->name; syn++) { + regfree(&syn->file_regex); + for (int j = 0; j < LENGTH(syn->rules); j++) { + SyntaxRule *rule = &syn->rules[j]; + if (!rule->rule) + break; + regfree(&rule->regex); + } + } + + ed->syntaxes = NULL; +} + +static void editor_windows_invalidate(Editor *ed, size_t pos, size_t len) { + size_t end = pos + len; + for (Win *win = ed->windows; win; win = win->next) { + if (ed->win != win && ed->win->text == win->text && + ((win->start <= pos && pos <= win->end) || + (win->start <= end && end <= win->end))) { + window_draw(win); + cursor_move_to(win, win->cursor.pos); + } + } +} + +static void editor_windows_insert(Editor *ed, size_t pos, const char *c, size_t len) { + text_insert_raw(ed->win->text, pos, c, len); + editor_windows_invalidate(ed, pos, len); +} + +static void editor_windows_delete(Editor *ed, size_t pos, size_t len) { + text_delete(ed->win->text, pos, len); + editor_windows_invalidate(ed, pos, len); +} + +size_t editor_delete(Editor *ed) { + Cursor *cursor = &ed->win->cursor; + Line *line = cursor->line; + size_t len = line->cells[cursor->col].len; + editor_windows_delete(ed, cursor->pos, len); + window_draw(ed->win); + return cursor_move_to(ed->win, cursor->pos); +} + +size_t editor_backspace(Editor *ed) { + Win *win = ed->win; + Cursor *cursor = &win->cursor; + if (win->start == cursor->pos) { + if (win->start == 0) + return cursor->pos; + /* if we are on the top left most position in the window + * first scroll up so that the to be deleted character is + * visible then proceed as normal */ + size_t pos = cursor->pos; + scroll_line_up(win, 1); + cursor_move_to(win, pos); + } + editor_char_prev(ed); + size_t pos = cursor->pos; + size_t len = cursor->line->cells[cursor->col].len; + editor_windows_delete(ed, pos, len); + window_draw(win); + return cursor_move_to(ed->win, pos); +} + +size_t editor_insert(Editor *ed, const char *c, size_t len) { + Win *win = ed->win; + size_t pos = win->cursor.pos; + editor_windows_insert(ed, pos, c, len); + if (win->cursor.line == win->bottomline && memchr(c, '\n', len)) + scroll_line_down(win, 1); + else + window_draw(win); + return cursor_move_to(win, pos + len); +} + +size_t editor_replace(Editor *ed, const char *c, size_t len) { + Win *win = ed->win; + Cursor *cursor = &win->cursor; + Line *line = cursor->line; + size_t pos = cursor->pos; + /* do not overwrite new line which would merge the two lines */ + if (line->cells[cursor->col].data != '\n') { + size_t oldlen = line->cells[cursor->col].len; + text_delete(win->text, pos, oldlen); + } + editor_windows_insert(ed, pos, c, len); + if (cursor->line == win->bottomline && memchr(c, '\n', len)) + scroll_line_down(win, 1); + else + window_draw(win); + return cursor_move_to(win, pos + len); +} + +size_t editor_cursor_get(Editor *ed) { + return ed->win->cursor.pos; +} + +size_t editor_selection_start(Editor *ed) { + return ed->win->sel.start = editor_cursor_get(ed); +} + +size_t editor_selection_end(Editor *ed) { + return ed->win->sel.end = editor_cursor_get(ed); +} + +Filerange editor_selection_get(Editor *ed) { + return window_selection_get(ed->win); +} + +void editor_selection_clear(Editor *ed) { + window_selection_clear(ed->win); +} + +size_t editor_line_goto(Editor *ed, size_t lineno) { + size_t pos = text_pos_by_lineno(ed->win->text, lineno); + return cursor_move_to(ed->win, pos); +} + +static void editor_search_forward(Editor *ed, Regex *regex) { + Win *win = ed->win; + Cursor *cursor = &win->cursor; + int pos = cursor->pos + 1; + int end = text_size(win->text); + RegexMatch match[1]; + bool found = false; + if (text_search_forward(win->text, pos, end - pos, regex, 1, match, 0)) { + pos = 0; + end = cursor->pos; + if (!text_search_forward(win->text, pos, end, regex, 1, match, 0)) + found = true; + } else { + found = true; + } + if (found) + cursor_move_to_line(win, match[0].start); +} + +static void editor_search_backward(Editor *ed, Regex *regex) { + Win *win = ed->win; + Cursor *cursor = &win->cursor; + int pos = 0; + int end = cursor->pos; + RegexMatch match[1]; + bool found = false; + if (text_search_backward(win->text, pos, end, regex, 1, match, 0)) { + pos = cursor->pos + 1; + end = text_size(win->text); + if (!text_search_backward(win->text, pos, end - pos, regex, 1, match, 0)) + found = true; + } else { + found = true; + } + if (found) + cursor_move_to_line(win, match[0].start); +} + +void editor_search(Editor *ed, const char *s, int direction) { + Regex *regex = text_regex_new(); + if (!regex) + return; + if (!text_regex_compile(regex, s, REG_EXTENDED)) { + if (direction >= 0) + editor_search_forward(ed, regex); + else + editor_search_backward(ed, regex); + } + text_regex_free(regex); +} + +void editor_snapshot(Editor *ed) { + text_snapshot(ed->win->text); +} + +void editor_undo(Editor *ed) { + Win *win = ed->win; + if (text_undo(win->text)) + window_draw(win); +} + +void editor_redo(Editor *ed) { + Win *win = ed->win; + if (text_redo(win->text)) + window_draw(win); +} + +static void editor_windows_arrange(Editor *ed) { + erase(); + wnoutrefresh(stdscr); + ed->windows_arrange(ed); +} + +static void editor_windows_arrange_horizontal(Editor *ed) { + int n = 0, x = 0, y = 0; + for (Win *win = ed->windows; win; win = win->next) + n++; + int height = ed->height / n; + for (Win *win = ed->windows; win; win = win->next) { + window_resize(win, ed->width, win->next ? height : ed->height - y); + window_move(win, x, y); + y += height; + } +} + +static void editor_windows_arrange_vertical(Editor *ed) { + int n = 0, x = 0, y = 0; + for (Win *win = ed->windows; win; win = win->next) + n++; + int width = ed->width / n; + for (Win *win = ed->windows; win; win = win->next) { + window_resize(win, win->next ? width : ed->width - x, ed->height); + window_move(win, x, y); + x += width; + } +} + +static void editor_window_split_internal(Editor *ed, const char *filename) { + Win *sel = ed->win; + editor_load(ed, filename); + if (sel && !filename) { + Win *win = ed->win; + text_free(win->text); + win->text = sel->text; + win->start = sel->start; + win->syntax = sel->syntax; + win->cursor.pos = sel->cursor.pos; + // TODO show begin of line instead of cursor->pos + } +} + +void editor_window_split(Editor *ed, const char *filename) { + editor_window_split_internal(ed, filename); + ed->windows_arrange = editor_windows_arrange_horizontal; + editor_windows_arrange(ed); +} + +void editor_window_vsplit(Editor *ed, const char *filename) { + editor_window_split_internal(ed, filename); + ed->windows_arrange = editor_windows_arrange_vertical; + editor_windows_arrange(ed); +} + +void editor_window_next(Editor *ed) { + Win *sel = ed->win; + if (!sel) + return; + ed->win = ed->win->next; + if (!ed->win) + ed->win = ed->windows; + window_statusbar_draw(sel); + window_statusbar_draw(ed->win); +} + +void editor_window_prev(Editor *ed) { + Win *sel = ed->win; + if (!sel) + return; + ed->win = ed->win->prev; + if (!ed->win) + for (ed->win = ed->windows; ed->win->next; ed->win = ed->win->next); + window_statusbar_draw(sel); + window_statusbar_draw(ed->win); +} + +void editor_mark_set(Editor *ed, Mark mark, size_t pos) { + text_mark_set(ed->win->text, mark, pos); +} + +void editor_mark_goto(Editor *ed, Mark mark) { + Win *win = ed->win; + size_t pos = text_mark_get(win->text, mark); + if (pos != (size_t)-1) + cursor_move_to_line(win, pos); +} + +void editor_mark_clear(Editor *ed, Mark mark) { + text_mark_clear(ed->win->text, mark); +} diff --git a/editor.h b/editor.h new file mode 100644 index 0000000..d36e503 --- /dev/null +++ b/editor.h @@ -0,0 +1,104 @@ +#include <curses.h> +#include <regex.h> + +typedef struct { + short fg, bg; /* fore and background color */ + int attr; /* curses attributes */ +} Color; + +typedef struct { + char *rule; /* regex to search for */ + int cflags; /* compilation flags (REG_*) used when compiling */ + Color color; /* settings to apply in case of a match */ + regex_t regex; /* compiled form of the above rule */ +} SyntaxRule; + +#define SYNTAX_REGEX_RULES 10 + +typedef struct { /* a syntax definition */ + char *name; /* syntax name */ + char *file; /* apply to files matching this regex */ + regex_t file_regex; /* compiled file name regex */ + SyntaxRule rules[SYNTAX_REGEX_RULES]; /* all rules for this file type */ +} Syntax; + +typedef struct Editor Editor; +typedef size_t Filepos; + +typedef struct { + Filepos start, end; /* range in bytes from start of file */ +} Filerange; + +typedef void (*editor_statusbar_t)(WINDOW *win, bool active, const char *filename, int line, int col); + +/* initialize a new editor with the available screen size */ +Editor *editor_new(int width, int height, editor_statusbar_t); +/* loads the given file and displays it in a new window. passing NULL as + * filename will open a new empty buffer */ +bool editor_load(Editor*, const char *filename); +size_t editor_insert(Editor*, const char *c, size_t len); +size_t editor_replace(Editor *ed, const char *c, size_t len); +size_t editor_backspace(Editor*); +size_t editor_delete(Editor*); +bool editor_resize(Editor*, int width, int height); +void editor_snapshot(Editor*); +void editor_undo(Editor*); +void editor_redo(Editor*); +void editor_draw(Editor *editor); +/* flush all changes made to the ncurses windows to the screen */ +void editor_update(Editor *editor); +void editor_free(Editor*); + +/* cursor movements, also updates selection if one is active, returns new cursor postion */ +size_t editor_file_begin(Editor *ed); +size_t editor_file_end(Editor *ed); +size_t editor_page_down(Editor *ed); +size_t editor_page_up(Editor *ed); +size_t editor_char_next(Editor*); +size_t editor_char_prev(Editor*); +size_t editor_line_goto(Editor*, size_t lineno); +size_t editor_line_down(Editor*); +size_t editor_line_up(Editor*); +size_t editor_line_begin(Editor*); +size_t editor_line_start(Editor*); +size_t editor_line_finish(Editor*); +size_t editor_line_end(Editor*); +size_t editor_word_end_next(Editor*); +size_t editor_word_end_prev(Editor *ed); +size_t editor_word_start_next(Editor*); +size_t editor_word_start_prev(Editor *ed); +size_t editor_sentence_next(Editor *ed); +size_t editor_sentence_prev(Editor *ed); +size_t editor_paragraph_next(Editor *ed); +size_t editor_paragraph_prev(Editor *ed); +size_t editor_bracket_match(Editor *ed); + +// mark handling +typedef int Mark; +void editor_mark_set(Editor*, Mark, size_t pos); +void editor_mark_goto(Editor*, Mark); +void editor_mark_clear(Editor*, Mark); + +// TODO comment +bool editor_syntax_load(Editor*, Syntax *syntaxes, Color *colors); +void editor_syntax_unload(Editor*); + +size_t editor_cursor_get(Editor*); +// TODO void return type? +size_t editor_selection_start(Editor*); +size_t editor_selection_end(Editor*); +Filerange editor_selection_get(Editor*); +void editor_selection_clear(Editor*); + +void editor_search(Editor *ed, const char *regex, int direction); + +void editor_window_split(Editor *ed, const char *filename); +void editor_window_vsplit(Editor *ed, const char *filename); +void editor_window_next(Editor *ed); +void editor_window_prev(Editor *ed); + +/* library initialization code, should be run at startup */ +void editor_init(void); +// TODO: comment +short editor_color_reserve(short fg, short bg); +short editor_color_get(short fg, short bg); @@ -0,0 +1,264 @@ +#define _POSIX_SOURCE +#include <locale.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <signal.h> +#include <errno.h> +#include <sys/select.h> +#include <sys/ioctl.h> + +#include "editor.h" +#include "util.h" + +#ifdef PDCURSES +int ESCDELAY; +#endif +#ifndef NCURSES_REENTRANT +# define set_escdelay(d) (ESCDELAY = (d)) +#endif + +typedef union { + size_t i; + const char *s; + size_t (*m)(Editor*); + void (*f)(Editor*); +} Arg; + +typedef struct { + char str[6]; + int code; +} Key; + +typedef struct { + Key key[2]; + void (*func)(const Arg *arg); + const Arg arg; +} KeyBinding; + +typedef struct Mode Mode; +struct Mode { + Mode *parent; + KeyBinding *bindings; + const char *name; + void (*enter)(void); + void (*leave)(void); + bool (*input)(const char *str, size_t len); +}; + +typedef struct { + char *name; + Mode *mode; +} Config; + +static void cursor(const Arg *arg); +static void call(const Arg *arg); +static void insert(const Arg *arg); +static void line(const Arg *arg); +static void find_forward(const Arg *arg); +static void find_backward(const Arg *arg); + +static Mode *mode; +static Editor *editor; + +#include "config.h" + +static Config *config = &editors[0]; + +static void cursor(const Arg *arg) { + arg->m(editor); +} + +static void call(const Arg *arg) { + arg->f(editor); +} + +static void line(const Arg *arg) { + editor_line_goto(editor, arg->i); +} + +static void find_forward(const Arg *arg) { + editor_search(editor, arg->s, 1); +} + +static void find_backward(const Arg *arg) { + editor_search(editor, arg->s, -1); +} + +static void insert(const Arg *arg) { + editor_insert(editor, arg->s, strlen(arg->s)); +} + +typedef struct Screen Screen; +static struct Screen { + int w, h; + bool need_resize; +} screen = { .need_resize = true }; + +static void sigwinch_handler(int sig) { + screen.need_resize = true; +} + +static void resize_screen(Screen *screen) { + struct winsize ws; + + if (ioctl(0, TIOCGWINSZ, &ws) == -1) { + getmaxyx(stdscr, screen->h, screen->w); + } else { + screen->w = ws.ws_col; + screen->h = ws.ws_row; + } + + resizeterm(screen->h, screen->w); + wresize(stdscr, screen->h, screen->w); + screen->need_resize = false; +} + +static void setup() { + setlocale(LC_CTYPE, ""); + if (!getenv("ESCDELAY")) + set_escdelay(50); + initscr(); + start_color(); + raw(); + noecho(); + keypad(stdscr, TRUE); + meta(stdscr, TRUE); + resize_screen(&screen); + /* needed because we use getch() which implicitly calls refresh() which + would clear the screen (overwrite it with an empty / unused stdscr */ + refresh(); + + struct sigaction sa; + sa.sa_flags = 0; + sigemptyset(&sa.sa_mask); + sa.sa_handler = sigwinch_handler; + sigaction(SIGWINCH, &sa, NULL); +} + +static void cleanup() { + editor_free(editor); + endwin(); +} + +static bool keymatch(Key *key0, Key *key1) { + return (key0->str[0] && memcmp(key0->str, key1->str, sizeof(key1->str)) == 0) || + (key0->code && key0->code == key1->code); +} + +static KeyBinding *keybinding(Mode *mode, Key *key0, Key *key1) { + for (; mode; mode = mode->parent) { + for (KeyBinding *kb = mode->bindings; kb && (kb->key[0].code || kb->key[0].str[0]); kb++) { + if (keymatch(key0, &kb->key[0]) && (!key1 || keymatch(key1, &kb->key[1]))) + return kb; + } + } + return NULL; +} + +int main(int argc, char *argv[]) { + /* decide which key configuration to use based on argv[0] */ + char *arg0 = argv[0]; + while (*arg0 && (*arg0 == '.' || *arg0 == '/')) + arg0++; + for (int i = 0; i < LENGTH(editors); i++) { + if (editors[i].name[0] == arg0[0]) { + config = &editors[i]; + break; + } + } + mode = config->mode; + + setup(); + if (!(editor = editor_new(screen.w, screen.h, statusbar))) + return 1; + if (!editor_syntax_load(editor, syntaxes, colors)) + return 1; + char *filename = argc > 1 ? argv[1] : NULL; + if (!editor_load(editor, filename)) + return 1; + + struct timeval idle = { .tv_usec = 0 }, *timeout = NULL; + Key key, key_prev, *key_mod = NULL; + + for (;;) { + if (screen.need_resize) { + resize_screen(&screen); + editor_resize(editor, screen.w, screen.h); + } + + fd_set fds; + FD_ZERO(&fds); + FD_SET(STDIN_FILENO, &fds); + + editor_update(editor); + doupdate(); + idle.tv_sec = 3; + int r = select(1, &fds, NULL, NULL, timeout); + if (r == -1 && errno == EINTR) + continue; + + if (r < 0) { + perror("select()"); + exit(EXIT_FAILURE); + } + + if (!FD_ISSET(STDIN_FILENO, &fds)) { + editor_snapshot(editor); + timeout = NULL; + continue; + } + + int keycode = getch(); + if (keycode == ERR) + continue; + + // TODO verbatim insert mode + int len = 0; + if (keycode >= KEY_MIN) { + key.code = keycode; + key.str[0] = '\0'; + } else { + char keychar = keycode; + key.str[len++] = keychar; + key.code = 0; + + if (!ISASCII(keychar) || keychar == '\e') { + nodelay(stdscr, TRUE); + for (int t; len < LENGTH(key.str) && (t = getch()) != ERR; len++) + key.str[len] = t; + nodelay(stdscr, FALSE); + } + } + + for (size_t i = len; i < LENGTH(key.str); i++) + key.str[i] = '\0'; + + KeyBinding *action = keybinding(mode, key_mod ? key_mod : &key, key_mod ? &key : NULL); + if (!action && key_mod) { + /* second char of a combination was invalid, search again without the prefix */ + action = keybinding(mode, &key, NULL); + key_mod = NULL; + } + if (action) { + /* check if it is the first part of a combination */ + if (!key_mod && (action->key[1].code || action->key[1].str[0])) { + key_prev = key; + key_mod = &key_prev; + continue; + } + action->func(&action->arg); + key_mod = NULL; + continue; + } + + if (keycode >= KEY_MIN) + continue; + + if (mode->input && mode->input(key.str, len)) + timeout = &idle; + } + + cleanup(); + return 0; +} |
