From bc0f09dce9fb9420ea1d5c10ebfacf50916b10af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Andr=C3=A9=20Tanner?= Date: Sun, 24 Aug 2014 10:04:28 +0200 Subject: Add work in progress editor frontend --- LICENSE | 13 + Makefile | 63 +++ colors.c | 106 +++++ config.def.h | 355 ++++++++++++++++ config.mk | 17 + editor.c | 1305 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ editor.h | 104 +++++ vis.c | 264 ++++++++++++ 8 files changed, 2227 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 colors.c create mode 100644 config.def.h create mode 100644 config.mk create mode 100644 editor.c create mode 100644 editor.h create mode 100644 vis.c diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0c1240b --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2014 Marc AndrĂ© Tanner + +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 +#include +#include + +#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 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 + * + * 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 +#include +#include +#include +#include +#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 +#include + +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); diff --git a/vis.c b/vis.c new file mode 100644 index 0000000..67483c5 --- /dev/null +++ b/vis.c @@ -0,0 +1,264 @@ +#define _POSIX_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +} -- cgit v1.2.3