aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--LICENSE13
-rw-r--r--Makefile63
-rw-r--r--colors.c106
-rw-r--r--config.def.h355
-rw-r--r--config.mk17
-rw-r--r--editor.c1305
-rw-r--r--editor.h104
-rw-r--r--vis.c264
8 files changed, 2227 insertions, 0 deletions
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 <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, &current))
+ 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);
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 <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;
+}