]> git.mdlowis.com Git - projs/tide.git/commitdiff
Replaced current implementation with gap-buffer and X11 ui
authorMichael D. Lowis <mike@mdlowis.com>
Sun, 25 Sep 2016 21:46:43 +0000 (17:46 -0400)
committerMichael D. Lowis <mike@mdlowis.com>
Sun, 25 Sep 2016 21:46:43 +0000 (17:46 -0400)
Makefile
buf.c [new file with mode: 0644]
config.mk [deleted file]
edit.h [new file with mode: 0644]
source/main.c [deleted file]
source/util.h [deleted file]
tests/atf.h [new file with mode: 0755]
tests/buf.c [new file with mode: 0644]
tests/tests.c [new file with mode: 0644]
xedit.c [new file with mode: 0644]

index 304fa69d36d47d6c65923ad224a91581b72fb049..bc11ad5171a5275b434fae5ff9c7884763365bff 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,79 +1,22 @@
-#------------------------------------------------------------------------------
-# Build Configuration
-#------------------------------------------------------------------------------
-# name and version
-PROJNAME = edit
-VERSION  = 0.0.1
+LDFLAGS  = -L/opt/X11/lib -lXft -lX11
+CFLAGS   = --std=c99 -Wall -Wextra -I. -I/opt/X11/include -I/opt/local/include/freetype2 -I/usr/include/freetype2
+SRCS     = xedit.c buf.c
+OBJS     = $(SRCS:.c=.o)
+TESTSRCS = tests/tests.c tests/buf.c
+TESTOBJS = $(TESTSRCS:.c=.o)
 
-# tools
-CC = c99
-LD = ${CC}
+all: edit test
 
-# flags
-INCS     = -Isource/
-CPPFLAGS = -D_XOPEN_SOURCE=700
-CFLAGS   = ${INCS} ${CPPFLAGS}
-LDFLAGS  = ${LIBS} -lncurses
+test: unittests
+       ./unittests
 
-# commands
-COMPILE = @echo CC $@; ${CC} ${CFLAGS} -c -o $@ $<
-LINK    = @echo LD $@; ${LD} -o $@ $^ ${LDFLAGS}
-CLEAN   = @rm -f
+edit: $(OBJS)
+       $(CC) -o $@ $^ $(LDFLAGS)
 
-#------------------------------------------------------------------------------
-# Build-Specific Macros
-#------------------------------------------------------------------------------
-# library macros
-BIN  = ${PROJNAME}
-DEPS = ${OBJS:.o=.d}
-OBJS = source/main.o
-
-# distribution dir and tarball settings
-DISTDIR   = ${PROJNAME}-${VERSION}
-DISTTAR   = ${DISTDIR}.tar
-DISTGZ    = ${DISTTAR}.gz
-DISTFILES = config.mk LICENSE.md Makefile README.md source
-
-# load user-specific settings
--include config.mk
-
-#------------------------------------------------------------------------------
-# Phony Targets
-#------------------------------------------------------------------------------
-.PHONY: all options tests
-
-all: options ${BIN}
-
-options:
-       @echo "Toolchain Configuration:"
-       @echo "  CC       = ${CC}"
-       @echo "  CFLAGS   = ${CFLAGS}"
-       @echo "  LD       = ${LD}"
-       @echo "  LDFLAGS  = ${LDFLAGS}"
-
-dist: clean
-       @echo DIST ${DISTGZ}
-       @mkdir -p ${DISTDIR}
-       @cp -R ${DISTFILES} ${DISTDIR}
-       @tar -cf ${DISTTAR} ${DISTDIR}
-       @gzip ${DISTTAR}
-       @rm -rf ${DISTDIR}
+unittests: $(TESTOBJS)
+       $(CC) -o $@ $^ $(LDFLAGS)
 
 clean:
-       ${CLEAN} ${DEPS}
-       ${CLEAN} ${BIN} ${OBJS}
-       ${CLEAN} ${OBJS:.o=.gcno} ${OBJS:.o=.gcda}
-       ${CLEAN} ${DISTTAR} ${DISTGZ}
-
-#------------------------------------------------------------------------------
-# Target-Specific Rules
-#------------------------------------------------------------------------------
-.c.o:
-       ${COMPILE}
-
-${BIN}: ${OBJS}
-       ${LINK}
-
-# load dependency files
--include ${DEPS}
+       $(RM) edit unittests $(OBJS) $(TESTOBJS)
 
+.PHONY: all test
diff --git a/buf.c b/buf.c
new file mode 100644 (file)
index 0000000..0213e2a
--- /dev/null
+++ b/buf.c
@@ -0,0 +1,180 @@
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "edit.h"
+
+void buf_load(Buf* buf, char* path)
+{
+    unsigned i = 0;
+    FILE* in = fopen(path, "rb");
+    int c;
+    while (EOF != (c = fgetc(in)))
+        buf_ins(buf, i++, (Rune)c);
+    fclose(in);
+}
+
+void buf_initsz(Buf* buf, size_t sz)
+{
+    buf->bufsize  = sz;
+    buf->bufstart = (Rune*)malloc(buf->bufsize * sizeof(Rune));
+    buf->bufend   = buf->bufstart + buf->bufsize;
+    buf->gapstart = buf->bufstart;
+    buf->gapend   = buf->bufend;
+}
+
+static void syncgap(Buf* buf, unsigned off)
+{
+    //printf("assert: %u <= %u\n", off, buf_end(buf));
+    assert(off <= buf_end(buf));
+    /* If the buffer is full, resize it before syncing */
+    if (0 == (buf->gapend - buf->gapstart)) {
+        Buf newbuf;
+        buf_initsz(&newbuf, buf->bufsize << 1);
+
+        for (Rune* curr = buf->bufstart; curr < buf->gapstart; curr++)
+            *(newbuf.gapstart++) = *(curr);
+        for (Rune* curr = buf->gapend; curr < buf->bufend; curr++)
+            *(newbuf.gapstart++) = *(curr);
+
+        free(buf->bufstart);
+        *buf = newbuf;
+    }
+
+    /* Move the gap to the desired offset */
+    Rune* newpos = (buf->bufstart + off);
+    if (newpos < buf->gapstart) {
+        while (newpos < buf->gapstart)
+            *(--buf->gapend) = *(--buf->gapstart);
+    } else {
+        while (newpos > buf->gapstart)
+            *(buf->gapstart++) = *(buf->gapend++);
+    }
+}
+
+void buf_init(Buf* buf)
+{
+    buf_initsz(buf, BufSize);
+}
+
+void buf_clr(Buf* buf)
+{
+    free(buf->bufstart);
+    buf_init(buf);
+}
+
+void buf_del(Buf* buf, unsigned off)
+{
+    syncgap(buf, off);
+    buf->gapend++;
+}
+
+void buf_ins(Buf* buf, unsigned off, Rune rune)
+{
+    syncgap(buf, off);
+    *(buf->gapstart++) = rune;
+}
+
+Rune buf_get(Buf* buf, unsigned off)
+{
+    if (off >= buf_end(buf)) return (Rune)'\n';
+    size_t bsz = (buf->gapstart - buf->bufstart);
+    if (off < bsz)
+        return *(buf->bufstart + off);
+    else
+        return *(buf->gapend + (off - bsz));
+}
+
+unsigned buf_bol(Buf* buf, unsigned off)
+{
+    if (off == 0) return 0;
+    if (buf_get(buf, off) == '\n' && buf_get(buf, off-1) == '\n') return off;
+    for (Rune r; (r = buf_get(buf, off)) != '\n'; off--);
+    return off+1;
+}
+
+unsigned buf_eol(Buf* buf, unsigned off)
+{
+    if (off >= buf_end(buf)) return off;
+    if (buf_get(buf, off) == '\n') return off;
+    for (Rune r; (r = buf_get(buf, off)) != '\n'; off++);
+    return off-1;
+}
+
+unsigned buf_end(Buf* buf)
+{
+    size_t bufsz = buf->bufend - buf->bufstart;
+    size_t gapsz = buf->gapend - buf->gapstart;
+    return (bufsz - gapsz);
+}
+
+static unsigned move_back(Buf* buf, unsigned pos) {
+    if (pos == 0) return 0;
+    if (buf_get(buf, pos)   == '\n' &&
+        buf_get(buf, pos-1) == '\n' &&
+        buf_get(buf, pos-2) != '\n')
+        pos = pos - 2;
+    else if (buf_get(buf, pos)   != '\n' &&
+             buf_get(buf, pos-1) == '\n' &&
+             buf_get(buf, pos-2) != '\n')
+        pos = pos - 2;
+    else
+        pos = pos - 1;
+    return pos;
+}
+
+static unsigned move_forward(Buf* buf, unsigned pos) {
+    if (pos+2 >= buf_end(buf)) return buf_end(buf)-2;
+    if (buf_get(buf, pos)   != '\n' &&
+        buf_get(buf, pos+1) == '\n')
+        pos = pos + 2;
+    else
+        pos = pos + 1;
+    return pos;
+}
+
+unsigned buf_byrune(Buf* buf, unsigned pos, int count)
+{
+    int move = (count < 0 ? -1 : 1);
+    count *= move;
+    for (; count > 0; count--)
+        pos = (move > 0 ? &move_forward : &move_back)(buf, pos);
+    return pos;
+}
+
+unsigned get_column(Buf* buf, unsigned pos)
+{
+    Rune r;
+    unsigned cols = 0;
+    if (buf_get(buf, pos) == '\n') return 0;
+    while ((r = buf_get(buf, pos)) != '\n')
+        pos--, cols += (r == '\t' ? TabWidth : 1);
+    return cols-1;
+}
+
+unsigned set_column(Buf* buf, unsigned pos, unsigned col)
+{
+    Rune r;
+    pos = buf_bol(buf, pos);
+    while ((r = buf_get(buf, pos)) != '\n' && col) {
+        unsigned inc = (r == '\t' ? TabWidth : 1);
+        pos++, col -= (inc > col ? col : inc);
+    }
+
+    if (buf_get(buf, pos)   == '\n' &&
+        buf_get(buf, pos-1) != '\n')
+        pos--;
+    return pos;
+}
+
+unsigned buf_byline(Buf* buf, unsigned pos, int count)
+{
+    int move = (count < 0 ? -1 : 1);
+    count *= move; // remove the sign if there is one
+    unsigned col = get_column(buf, pos);
+    for (; count > 0; count--)
+        pos = (move < 0 ? move_back(buf, buf_bol(buf, pos))
+                        : move_forward(buf, buf_eol(buf, pos)));
+    return set_column(buf, pos, col);
+}
diff --git a/config.mk b/config.mk
deleted file mode 100644 (file)
index 2176092..0000000
--- a/config.mk
+++ /dev/null
@@ -1,17 +0,0 @@
-#------------------------------------------------------------------------------
-# User and Platform Specific Configuration Options
-#------------------------------------------------------------------------------
-# Override the tools used with platform specific ones
-#CC = cc
-#LD = ${CC}
-#AR = ar
-
-# GCC dependency generation
-#COMPILE += && gcc ${INCS} -MM -MT $@ -MF ${@:.o=.d} ${<:.o=.c}
-
-# Enable output of debug symbols
-#CFLAGS += -g
-
-# Enable output of coverage information
-#CFLAGS  += --coverage
-#LDFLAGS += --coverage
diff --git a/edit.h b/edit.h
new file mode 100644 (file)
index 0000000..1faf6f9
--- /dev/null
+++ b/edit.h
@@ -0,0 +1,84 @@
+/* Definitons
+ *****************************************************************************/
+enum ColorId {
+    CLR_BASE03 = 0,
+    CLR_BASE02,
+    CLR_BASE01,
+    CLR_BASE00,
+    CLR_BASE0,
+    CLR_BASE1,
+    CLR_BASE2,
+    CLR_BASE3,
+    CLR_YELLOW,
+    CLR_ORANGE,
+    CLR_RED,
+    CLR_MAGENTA,
+    CLR_VIOLET,
+    CLR_BLUE,
+    CLR_CYAN,
+    CLR_GREEN,
+};
+
+typedef uint32_t Color;
+typedef uint32_t Rune;
+
+/* Buffer management functions
+ *****************************************************************************/
+typedef struct buf {
+    size_t bufsize;
+    Rune* bufstart;
+    Rune* bufend;
+    Rune* gapstart;
+    Rune* gapend;
+} Buf;
+
+void buf_load(Buf* buf, char* path);
+void buf_initsz(Buf* buf, size_t sz);
+void buf_init(Buf* buf);
+void buf_clr(Buf* buf);
+void buf_del(Buf* buf, unsigned pos);
+void buf_ins(Buf* buf, unsigned pos, Rune);
+Rune buf_get(Buf* buf, unsigned pos);
+unsigned buf_bol(Buf* buf, unsigned pos);
+unsigned buf_eol(Buf* buf, unsigned pos);
+unsigned buf_end(Buf* buf);
+unsigned buf_byrune(Buf* buf, unsigned pos, int count);
+unsigned buf_byline(Buf* buf, unsigned pos, int count);
+
+/* Miscellaneous Functions
+ *****************************************************************************/
+void die(char* msg);
+
+/* Configuration
+ *****************************************************************************/
+enum {
+    Width     = 640,
+    Height    = 480,
+    TabWidth  = 4,
+    BufSize   = 8192,
+};
+
+static enum { DARK = 0, LIGHT = 1 } ColorBase = DARK;
+
+static Color Palette[][2] = {
+/*   Color Name   =   Dark      Light    */
+    [CLR_BASE03]  = { 0x002b36, 0xfdf6e3 },
+    [CLR_BASE02]  = { 0x073642, 0xeee8d5 },
+    [CLR_BASE01]  = { 0x586e75, 0x93a1a1 },
+    [CLR_BASE00]  = { 0x657b83, 0x839496 },
+    [CLR_BASE0]   = { 0x839496, 0x657b83 },
+    [CLR_BASE1]   = { 0x93a1a1, 0x586e75 },
+    [CLR_BASE2]   = { 0xeee8d5, 0x073642 },
+    [CLR_BASE3]   = { 0xfdf6e3, 0x002b36 },
+    [CLR_YELLOW]  = { 0xb58900, 0xb58900 },
+    [CLR_ORANGE]  = { 0xcb4b16, 0xcb4b16 },
+    [CLR_RED]     = { 0xdc322f, 0xdc322f },
+    [CLR_MAGENTA] = { 0xd33682, 0xd33682 },
+    [CLR_VIOLET]  = { 0x6c71c4, 0x6c71c4 },
+    [CLR_BLUE]    = { 0x268bd2, 0x268bd2 },
+    [CLR_CYAN]    = { 0x2aa198, 0x2aa198 },
+    [CLR_GREEN]   = { 0x859900, 0x859900 },
+};
+
+#define FONTNAME "Monaco:pixelsize=15:antialias=true:autohint=true"
+
diff --git a/source/main.c b/source/main.c
deleted file mode 100644 (file)
index 8f86979..0000000
+++ /dev/null
@@ -1,258 +0,0 @@
-/* Includes
- *****************************************************************************/
-static void cleanup(void);
-#define CLEANUP_HOOK cleanup
-#include <util.h>
-#include <ncurses.h>
-
-/* Type Definitions
- *****************************************************************************/
-typedef struct Line {
-    struct Line* prev;
-    struct Line* next;
-    size_t length;
-    char* text;
-} Line;
-
-typedef struct {
-    char* name;
-    Line* start;
-    Line* first;
-    Line* last;
-} File;
-
-typedef struct {
-    int x;
-    int y;
-} Pos;
-
-typedef struct {
-    Line* line;
-    int offset;
-} FilePos;
-
-/* Globals
- *****************************************************************************/
-static bool ScreenDirty = true;
-static File CurrFile = { .name = NULL, .start = NULL, .first = NULL, .last = NULL };
-static Pos Curr = { .x = 0, .y = 0 };
-static Pos Max  = { .x = 0, .y = 0 };
-static FilePos Loc = { .line = NULL, .offset = 0 };
-
-/* Macros
- *****************************************************************************/
-#define line_length()    (Loc.line->length)
-#define visible_length() (Loc.line->length-2 - Loc.offset)
-
-/* Declarations
- *****************************************************************************/
-static void setup(void);
-static void load(char* fname);
-static void edit(void);
-static void input(int ch);
-static void cursor_left(void);
-static void cursor_down(void);
-static void cursor_up(void);
-static void cursor_right(void);
-static void cursor_home(void);
-static void cursor_end(void);
-
-/* Main Routine
- *****************************************************************************/
-int main(int argc, char** argv)
-{
-    atexit(cleanup);
-    setup();
-    if (argc > 1) {
-        load(argv[1]);
-        edit();
-    } else {
-        die("no filename provided");
-    }
-    return EXIT_SUCCESS;
-}
-
-/* Definitions
- *****************************************************************************/
-static void setup(void)
-{
-    initscr();
-    getmaxyx(stdscr, Max.y, Max.x);
-    raw();
-    keypad(stdscr, true);
-    noecho();
-}
-
-static void cleanup(void)
-{
-    /* shutdown ncurses */
-    endwin();
-    /* free up malloced data */
-    free(CurrFile.name);
-    Line* line = CurrFile.first;
-    while (line) {
-        Line* deadite = line;
-        line = line->next;
-        free(deadite->text);
-        free(deadite);
-    }
-}
-
-static void load(char* fname)
-{
-    FILE* file = efopen(fname, "r");
-    CurrFile.name = estrdup(fname);
-    while (!feof(file)) {
-        Line* line = (Line*)ecalloc(1,sizeof(Line));
-        line->text = efreadline(file);
-        line->length = strlen(line->text);
-        if (CurrFile.first == NULL) {
-            CurrFile.start = line;
-            CurrFile.first = line;
-            CurrFile.last  = line;
-            Loc.line = line;
-        } else {
-            CurrFile.last->next = line;
-            line->prev = CurrFile.last;
-            CurrFile.last = line;
-        }
-    }
-    fclose(file);
-}
-
-static void edit(void)
-{
-    int ch = 0, x = 0;
-    do {
-        /* Handle input */
-        input(ch);
-        /* Refresh the screen */
-        if (ScreenDirty) {
-            clear();
-            Line* line = CurrFile.start;
-            for (int i = 0; (i < Max.y) && line; i++, line = line->next) {
-                if (line->length > Loc.offset) {
-                    mvprintw(i, 0, "%s", &(line->text[Loc.offset]));
-                }
-            }
-            refresh();
-            ScreenDirty = false;
-        }
-        /* Place the cursor */
-        if (line_length() <= 1)
-            x = 0;
-        else if (Curr.x > visible_length())
-            x = visible_length();
-        else
-            x = Curr.x;
-        move(Curr.y, x);
-    } while((ch = getch()) != 'q');
-}
-
-static void input(int ch)
-{
-    switch (ch) {
-        case KEY_LEFT:
-        case 'h':
-            cursor_left();
-            break;
-
-        case KEY_DOWN:
-        case 'j':
-            cursor_down();
-            break;
-
-        case KEY_UP:
-        case 'k':
-            cursor_up();
-            break;
-
-        case KEY_RIGHT:
-        case 'l':
-            cursor_right();
-            break;
-
-        case KEY_HOME:
-        case '0':
-            cursor_home();
-            break;
-
-        case KEY_END:
-        case '$':
-            cursor_end();
-            break;
-    }
-}
-
-static void cursor_left(void)
-{
-    Curr.x--;
-    if (Curr.x < 0) {
-        Curr.x = 0;
-        Loc.offset--;
-        if (Loc.offset < 0)
-            Loc.offset = 0;
-        ScreenDirty = true;
-    } else if (Curr.x > visible_length()) {
-        Curr.x = visible_length()-1;
-    }
-}
-
-static void cursor_down(void)
-{
-    if (Loc.line->next) {
-        Curr.y++;
-        if (Curr.y >= Max.y) {
-            Curr.y = Max.y-1;
-            if (CurrFile.start->next) {
-                CurrFile.start = CurrFile.start->next;
-                ScreenDirty = true;
-            }
-        }
-        Loc.line = Loc.line->next;
-    }
-}
-
-static void cursor_up(void)
-{
-    if (Loc.line->prev) {
-        Curr.y--;
-        if (Curr.y < 0) {
-            Curr.y = 0;
-            if (CurrFile.start->prev) {
-                CurrFile.start = CurrFile.start->prev;
-                ScreenDirty = true;
-            }
-        }
-        Loc.line = Loc.line->prev;
-    }
-}
-
-static void cursor_right(void)
-{
-    if ((line_length() > 1) && (Curr.x < visible_length()))
-    {
-        Curr.x++;
-        if (Curr.x >= Max.x) {
-            Curr.x = Max.x-1;
-            Loc.offset++;
-            if (Loc.offset > visible_length())
-                Loc.offset = visible_length();
-            ScreenDirty = true;
-        }
-    }
-}
-
-static void cursor_home(void)
-{
-    Curr.x = 0;
-    Loc.offset = 0;
-    ScreenDirty = true;
-}
-
-static void cursor_end(void)
-{
-    Curr.x = ((line_length() <= 1) ? 0 : visible_length());
-    Loc.offset = (line_length() > Max.x) ? line_length()-1 - Max.x : Loc.offset;
-    ScreenDirty = true;
-}
diff --git a/source/util.h b/source/util.h
deleted file mode 100644 (file)
index d32bcef..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-#ifndef UTIL_H
-#define UTIL_H
-
-/* Standard Macros and Types */
-#include <stddef.h>
-#include <stdint.h>
-#include <stdbool.h>
-#include <errno.h>
-
-/* Useful Standard Functions */
-#include <signal.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <stdarg.h>
-#include <string.h>
-
-/* Generic Death Function */
-static void die(const char* msgfmt, ...)
-{
-    va_list args;
-    va_start(args, msgfmt);
-    #ifdef CLEANUP_HOOK
-    CLEANUP_HOOK();
-    #endif
-    fprintf(stderr, "Error: ");
-    vfprintf(stderr, msgfmt, args);
-    fprintf(stderr, "\n");
-    va_end(args);
-    exit(EXIT_FAILURE);
-}
-
-/* Signal Handling */
-static void esignal(int sig, void (*func)(int))
-{
-    errno = 0;
-    func  = signal(sig, func);
-    if (func == SIG_ERR || errno > 0)
-        die("failed to register signal handler for signal %d", sig);
-}
-
-static int eraise(int sig)
-{
-    int ret;
-    if ((ret = raise(sig)) != 0)
-        die("failed to raise signal %d", sig);
-    return ret;
-}
-
-/* Dynamic Allocation */
-static void* ecalloc(size_t num, size_t size)
-{
-    void* ret;
-    if (NULL == (ret = calloc(num,size)))
-        die("out of memory");
-    return ret;
-}
-
-static void* emalloc(size_t size)
-{
-    void* ret;
-    if (NULL == (ret = malloc(size)))
-        die("out of memory");
-    return ret;
-}
-
-static void* erealloc(void* ptr, size_t size)
-{
-    void* ret;
-    if (NULL == (ret = realloc(ptr,size)))
-        die("out of memory");
-    return ret;
-}
-
-/* File Handling */
-static FILE* efopen(const char* filename, const char* mode)
-{
-    FILE* file;
-    errno = 0;
-    if (NULL == (file = fopen(filename, mode)) || errno != 0)
-        die("failed to open file: %s", filename);
-    return file;
-}
-
-static char* efreadline(FILE* input)
-{
-    size_t size  = 8;
-    size_t index = 0;
-    char*  str   = (char*)emalloc(size);
-    memset(str, 0, 8);
-    if (feof(input)) {
-        free(str);
-        return NULL;
-    }
-    while (true) {
-        char ch = fgetc(input);
-        if (ch == EOF) break;
-        str[index++] = ch;
-        str[index]   = '\0';
-        if (index+1 >= size) {
-            size = size << 1;
-            str  = erealloc(str, size);
-        }
-        if (ch == '\n') break;
-    }
-    return str;
-}
-
-/* String Handling */
-char* estrdup(const char *s)
-{
-    char* ns = (char*)emalloc(strlen(s) + 1);
-    strcpy(ns,s);
-    return ns;
-}
-
-#endif
diff --git a/tests/atf.h b/tests/atf.h
new file mode 100755 (executable)
index 0000000..b0f07c7
--- /dev/null
@@ -0,0 +1,125 @@
+/**
+  @brief A minimalistic unit testing framework for C.
+  @author Michael D. Lowis
+  @license BSD 2-clause License
+*/
+#ifndef ATF_H
+#define ATF_H
+
+#include <stddef.h>
+#include <stdbool.h>
+
+extern char* Curr_Test;
+void atf_init(int argc, char** argv);
+void atf_test_start(char* file, unsigned int line, char* name);
+bool atf_test_assert(bool success, char* expr_str, char* file, int line);
+void atf_test_fail(char* expr, char* file, int line);
+int atf_print_results(void);
+
+#define CHECK(expr) \
+    if(atf_test_assert((expr), #expr, __FILE__, __LINE__)) break
+
+#define TEST_SUITE(name) \
+    void name(void)
+
+#define TEST(desc) \
+    for(atf_test_start(__FILE__,__LINE__,#desc); Curr_Test != NULL; Curr_Test = NULL)
+
+#define RUN_TEST_SUITE(name) \
+    name();
+
+#define RUN_EXTERN_TEST_SUITE(name) \
+    do { extern TEST_SUITE(name); RUN_TEST_SUITE(name); } while(0)
+
+#define PRINT_TEST_RESULTS \
+    atf_print_results
+
+/* Function Definitions
+ *****************************************************************************/
+#ifdef INCLUDE_DEFS
+#include <stdio.h>
+#include <stdlib.h>
+#ifndef NO_SIGNALS
+#include <signal.h>
+#endif
+
+char* Curr_Test = NULL;
+char* Curr_File = NULL;
+unsigned int Curr_Line = 0;
+static unsigned int Total = 0;
+static unsigned int Failed = 0;
+
+#ifndef NO_SIGNALS
+static void handle_signal(int sig) {
+    /* Determine the signal name */
+    char* sig_name = NULL;
+    switch(sig) {
+        case SIGABRT: sig_name = "SIGABRT"; break;
+        case SIGBUS:  sig_name = "SIGBUS";  break;
+        case SIGFPE:  sig_name = "SIGFPE";  break;
+        case SIGILL:  sig_name = "SIGILL";  break;
+        case SIGSEGV: sig_name = "SIGSEGV"; break;
+        case SIGSYS:  sig_name = "SIGSYS";  break;
+        /* If we don't recognize it then just return and let the default handler
+           catch it. */
+        default:      return;
+    }
+    /* Error and exit. No summary will be printed but the user will know which
+       test has crashed. */
+    fprintf(stderr,"%s:%d:0:%s:CRASH (signal: %d - %s)\n", Curr_File, Curr_Line, Curr_Test, sig, sig_name);
+    Failed++;
+    (void)atf_print_results();
+    exit(1);
+}
+#endif
+
+void atf_init(int argc, char** argv) {
+    /* I reserve the right to use these later */
+    (void)argc;
+    (void)argv;
+
+#ifndef NO_SIGNALS
+    /* Init signal handler */
+    signal(SIGABRT, handle_signal);
+    signal(SIGBUS,  handle_signal);
+    signal(SIGFPE,  handle_signal);
+    signal(SIGILL,  handle_signal);
+    signal(SIGSEGV, handle_signal);
+    signal(SIGSYS,  handle_signal);
+#endif
+}
+
+void atf_test_start(char* file, unsigned int line, char* name) {
+    Curr_File = file;
+    Curr_Line = line;
+    Curr_Test = name;
+    Total++;
+}
+
+bool atf_test_assert(bool success, char* expr, char* file, int line) {
+    bool failed = !success;
+    if (failed) atf_test_fail(expr,file,line);
+    return failed;
+}
+
+void atf_test_fail(char* expr, char* file, int line) {
+    Failed++;
+    printf("%s:%d:0:%s:FAIL:( %s )\n", file, line, Curr_Test, expr); \
+}
+
+int atf_print_results(void) {
+    static const char* results_string =
+    "\nUnit Test Summary"
+    "\n-----------------"
+    "\nTotal:  %d"
+    "\nPassed: %d"
+    "\nFailed: %d"
+    "\n\n";
+    printf(results_string, Total, Total - Failed, Failed);
+    return Failed;
+}
+
+#undef INCLUDE_DEFS
+#endif
+
+#endif /* ATF_H */
diff --git a/tests/buf.c b/tests/buf.c
new file mode 100644 (file)
index 0000000..2977917
--- /dev/null
@@ -0,0 +1,190 @@
+#include "atf.h"
+#include "../buf.c"
+
+static Buf TestBuf;
+
+static void set_buffer_text(char* str) {
+    int i = 0;
+    buf_clr(&TestBuf);
+    for (Rune* curr = TestBuf.bufstart; curr < TestBuf.bufend; curr++)
+        *curr = '-';
+    while (*str)
+        buf_ins(&TestBuf, i++, (Rune)*str++);
+}
+
+static bool buf_text_eq(char* str) {
+    for (unsigned i = 0; i < buf_end(&TestBuf); i++) {
+//        printf("%c", (char)buf_get(&TestBuf, i));
+        if ((Rune)*(str++) != buf_get(&TestBuf, i))
+            return false;
+    }
+//    puts("");
+    return true;
+}
+
+TEST_SUITE(BufferTests) {
+    /* Init and Clear
+     *************************************************************************/
+    TEST(buf_initsz should init the buffer to the given size) {
+        buf_initsz(&TestBuf, 1024);
+        CHECK(TestBuf.bufsize == 1024);
+        CHECK(TestBuf.bufstart == TestBuf.gapstart);
+        CHECK(TestBuf.bufend == TestBuf.gapend);
+        CHECK(TestBuf.bufend - TestBuf.bufstart == 1024);
+        CHECK(TestBuf.gapend - TestBuf.gapstart == 1024);
+        free(TestBuf.bufstart);
+    }
+
+    TEST(buf_init should init the buffer to the default size) {
+        buf_init(&TestBuf);
+        CHECK(TestBuf.bufsize == BufSize);
+        CHECK(TestBuf.bufstart == TestBuf.gapstart);
+        CHECK(TestBuf.bufend == TestBuf.gapend);
+        CHECK(TestBuf.bufend - TestBuf.bufstart == BufSize);
+        CHECK(TestBuf.gapend - TestBuf.gapstart == BufSize);
+    }
+
+    TEST(buf_clr should clear the buffer) {
+        buf_initsz(&TestBuf, 1024);
+        buf_clr(&TestBuf);
+        CHECK(TestBuf.bufsize == BufSize);
+        CHECK(TestBuf.bufstart == TestBuf.gapstart);
+        CHECK(TestBuf.bufend == TestBuf.gapend);
+        CHECK(TestBuf.bufend - TestBuf.bufstart == BufSize);
+        CHECK(TestBuf.gapend - TestBuf.gapstart == BufSize);
+    }
+
+    /* Insertions
+     *************************************************************************/
+    TEST(buf_ins should insert at 0 in empty buf) {
+        buf_clr(&TestBuf);
+        buf_ins(&TestBuf, 0, 'a');
+        CHECK(buf_text_eq("a"));
+    }
+
+    TEST(buf_ins should insert at 0) {
+        buf_clr(&TestBuf);
+        buf_ins(&TestBuf, 0, 'b');
+        buf_ins(&TestBuf, 0, 'a');
+        CHECK(buf_text_eq("ab"));
+    }
+
+    TEST(buf_ins should insert at 1) {
+        buf_clr(&TestBuf);
+        buf_ins(&TestBuf, 0, 'a');
+        buf_ins(&TestBuf, 1, 'b');
+        CHECK(buf_text_eq("ab"));
+    }
+
+    TEST(buf_ins should insert at 1) {
+        buf_clr(&TestBuf);
+        buf_ins(&TestBuf, 0, 'a');
+        buf_ins(&TestBuf, 1, 'c');
+        buf_ins(&TestBuf, 1, 'b');
+        CHECK(buf_text_eq("abc"));
+    }
+
+    TEST(buf_ins should sentence in larger text) {
+        set_buffer_text(
+            "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam elementum eros quis venenatis. "
+        );
+
+        buf_ins(&TestBuf, 5, ' ');
+        buf_ins(&TestBuf, 6, 'a');
+
+        CHECK(buf_text_eq(
+            "Lorem a ipsum dolor sit amet, consectetur adipiscing elit. Aliquam elementum eros quis venenatis. "
+        ));
+    }
+
+    /* Deletions
+     *************************************************************************/
+
+    /* Beginning and End of line
+     *************************************************************************/
+    TEST(buf_bol should move to first non newline character of line) {
+        set_buffer_text("\nabc\n");
+        CHECK(1 == buf_bol(&TestBuf, 3));
+    }
+
+    TEST(buf_bol should do nothing for blank line) {
+        set_buffer_text("\n\n");
+        CHECK(1 == buf_bol(&TestBuf, 1));
+    }
+
+    TEST(buf_eol should move to last non newline character of line) {
+        set_buffer_text("\nabc\n");
+        CHECK(3 == buf_eol(&TestBuf, 1));
+    }
+
+    TEST(buf_eol should do nothing for blank line) {
+        set_buffer_text("\n\n");
+        CHECK(1 == buf_eol(&TestBuf, 1));
+    }
+
+    /* Movement by Rune
+     *************************************************************************/
+    TEST(buf_byrune should do nothing for -1 at beginning of file)
+    {
+        set_buffer_text("abc\n");
+        CHECK(0 == buf_byrune(&TestBuf, 0, -1));
+    }
+
+    TEST(buf_byrune should do nothing for -2 at beginning of file)
+    {
+        set_buffer_text("abc\n");
+        CHECK(0 == buf_byrune(&TestBuf, 0, -2));
+    }
+
+    TEST(buf_byrune should do nothing for +1 at end of file)
+    {
+        set_buffer_text("abc\n");
+        CHECK(2 == buf_byrune(&TestBuf, 2, 1));
+    }
+
+    TEST(buf_byrune should do nothing for +2 at end of file)
+    {
+        set_buffer_text("abc\n");
+        CHECK(2 == buf_byrune(&TestBuf, 2, 2));
+    }
+
+    TEST(buf_byrune should skip newlines for -1)
+    {
+        set_buffer_text("ab\ncd\n");
+        CHECK(1 == buf_byrune(&TestBuf, 3, -1));
+    }
+
+    TEST(buf_byrune should skip newlines for +1)
+    {
+        set_buffer_text("ab\ncd\n");
+        CHECK(3 == buf_byrune(&TestBuf, 1, 1));
+    }
+
+    TEST(buf_byrune should not skip blank lines for -1)
+    {
+        set_buffer_text("ab\n\ncd\n");
+        CHECK(3 == buf_byrune(&TestBuf, 4, -1));
+    }
+
+    TEST(buf_byrune should not skip blank lines for +1)
+    {
+        set_buffer_text("ab\n\ncd\n");
+        CHECK(3 == buf_byrune(&TestBuf, 1, 1));
+    }
+
+    TEST(buf_byrune should move from blank line to non-blank line for -1)
+    {
+        set_buffer_text("ab\n\ncd\n");
+        CHECK(1 == buf_byrune(&TestBuf, 3, -1));
+    }
+
+    TEST(buf_byrune should move from blank line to non-blank line for +1)
+    {
+        set_buffer_text("ab\n\ncd\n");
+        CHECK(4 == buf_byrune(&TestBuf, 3, 1));
+    }
+
+    /* Movement by Line
+     *************************************************************************/
+
+}
diff --git a/tests/tests.c b/tests/tests.c
new file mode 100644 (file)
index 0000000..d1465d6
--- /dev/null
@@ -0,0 +1,8 @@
+#define INCLUDE_DEFS
+#include "atf.h"
+
+int main(int argc, char** argv) {
+    atf_init(argc,argv);
+    RUN_EXTERN_TEST_SUITE(BufferTests);
+    return atf_print_results();
+}
diff --git a/xedit.c b/xedit.c
new file mode 100644 (file)
index 0000000..601ff49
--- /dev/null
+++ b/xedit.c
@@ -0,0 +1,256 @@
+#include <signal.h>
+#include <stdio.h>
+#include <stdbool.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/Xft/Xft.h>
+#include <X11/cursorfont.h>
+
+#include "edit.h"
+
+/*****************************************************************************/
+
+struct {
+    Display* display;
+    Visual* visual;
+    Colormap colormap;
+    unsigned depth;
+    int screen;
+    GC gc;
+    XftFont* font;
+    Window window;
+    Pixmap pixmap;
+    XftDraw* xft;
+    int width;
+    int height;
+    XIC xic;
+    XIM xim;
+} X;
+Buf Buffer;
+unsigned StartRow = 0;
+unsigned CursorPos = 0;
+unsigned LastDrawnPos = 0;
+
+void die(char* m) {
+    fprintf(stderr, "dying, %s\n", m);
+    exit(1);
+}
+
+static int init(void) {
+    signal(SIGPIPE, SIG_IGN); // Ignore the SIGPIPE signal
+    /* open the X display and get basic attributes */
+    if (!(X.display = XOpenDisplay(0)))
+        die("cannot open display");
+    Window root = DefaultRootWindow(X.display);
+    XWindowAttributes wa;
+    XGetWindowAttributes(X.display, root, &wa);
+    X.visual   = wa.visual;
+    X.colormap = wa.colormap;
+    X.screen   = DefaultScreen(X.display);
+    X.depth    = DefaultDepth(X.display, X.screen);
+
+    /* create the main window */
+    X.window = XCreateSimpleWindow(X.display, root, 0, 0, Width, Height, 0, 0, WhitePixel(X.display, X.screen));
+    XSetWindowAttributes swa;
+    swa.backing_store = WhenMapped;
+    swa.bit_gravity = NorthWestGravity;
+    XChangeWindowAttributes(X.display, X.window, CWBackingStore|CWBitGravity, &swa);
+    XStoreName(X.display, X.window, "edit");
+    XSelectInput(X.display, X.window, StructureNotifyMask|ButtonPressMask|ButtonReleaseMask|Button1MotionMask|KeyPressMask|ExposureMask|FocusChangeMask);
+
+    /* simulate an initial resize and map the window */
+    XConfigureEvent ce;
+    ce.type   = ConfigureNotify;
+    ce.width  = Width;
+    ce.height = Height;
+    XSendEvent(X.display, X.window, False, StructureNotifyMask, (XEvent *)&ce);
+    XMapWindow(X.display, X.window);
+
+    /* set input methods */
+    if((X.xim = XOpenIM(X.display, 0, 0, 0)))
+        X.xic = XCreateIC(X.xim, XNInputStyle, XIMPreeditNothing|XIMStatusNothing, XNClientWindow, X.window, XNFocusWindow, X.window, NULL);
+
+    /* allocate font */
+    if(!(X.font = XftFontOpenName(X.display, X.screen, FONTNAME)))
+        die("cannot open default font");
+
+    /* initialize the graphics context */
+    XGCValues gcv;
+    gcv.foreground = WhitePixel(X.display, X.screen);
+    gcv.graphics_exposures = False;
+    X.gc = XCreateGC(X.display, X.window, GCForeground|GCGraphicsExposures, &gcv);
+
+    /* initialize pixmap and drawing context */
+    X.pixmap = XCreatePixmap(X.display, X.window, Width, Height, X.depth);
+    X.xft = XftDrawCreate(X.display, X.pixmap, X.visual, X.colormap);
+
+    return XConnectionNumber(X.display);
+}
+
+static void deinit(void) {
+    if (X.pixmap != None) {
+        XftDrawDestroy(X.xft);
+        XFreePixmap(X.display, X.pixmap);
+    }
+    XCloseDisplay(X.display);
+}
+
+static void handle_key(XEvent* e) {
+    int len;
+    char buf[8];
+    KeySym key;
+    Status status;
+    /* Read the key string */
+    if (X.xic)
+        len = Xutf8LookupString(X.xic, &e->xkey, buf, sizeof(buf), &key, &status);
+    else
+        len = XLookupString(&e->xkey, buf, sizeof(buf), &key, 0);
+    /* Handle the key */
+    switch (key) {
+        case XK_F6:
+            ColorBase = !ColorBase;
+            break;
+
+        case XK_Left:
+            CursorPos = buf_byrune(&Buffer, CursorPos, -1);
+            break;
+
+        case XK_Right:
+            CursorPos = buf_byrune(&Buffer, CursorPos, 1);
+            break;
+
+        case XK_Down:
+            CursorPos = buf_byline(&Buffer, CursorPos, 1);
+            break;
+
+        case XK_Up:
+            CursorPos = buf_byline(&Buffer, CursorPos, -1);
+            break;
+    }
+}
+
+static void handle_mousebtn(XEvent* e) {
+    switch (e->xbutton.button) {
+        case Button1: /* Left Button */
+            break;
+        case Button2: /* Middle Button */
+            break;
+        case Button3: /* Right Button */
+            break;
+        case Button4: /* Wheel Up */
+            StartRow = buf_byline(&Buffer, StartRow, -2);
+            break;
+        case Button5: /* Wheel Down */
+            StartRow = buf_byline(&Buffer, StartRow, 2);
+            break;
+    }
+}
+
+static void handle_event(XEvent* e) {
+    switch (e->type) {
+        case FocusIn:
+            if (X.xic) XSetICFocus(X.xic);
+            break;
+
+        case FocusOut:
+            if (X.xic) XUnsetICFocus(X.xic);
+            break;
+
+        case Expose: // Draw the window
+            XCopyArea(X.display, X.pixmap, X.window, X.gc, 0, 0, X.width, X.height, 0, 0);
+            XFlush(X.display);
+            break;
+
+        case ConfigureNotify: // Resize the window
+            if (e->xconfigure.width != X.width || e->xconfigure.height != X.height) {
+                X.width  = e->xconfigure.width;
+                X.height = e->xconfigure.height;
+                X.pixmap = XCreatePixmap(X.display, X.window, X.width, X.height, X.depth);
+                X.xft    = XftDrawCreate(X.display, X.pixmap, X.visual, X.colormap);
+            }
+            break;
+
+        case ButtonPress: // Mouse button handling
+            handle_mousebtn(e);
+            break;
+
+        case KeyPress: // Keyboard events
+            handle_key(e);
+            break;
+    }
+}
+
+static XftColor xftcolor(enum ColorId cid, uint16_t alpha) {
+    Color c = Palette[cid][ColorBase];
+    XftColor xc;
+    xc.color.red   = ((c & 0x00FF0000) >> 8);
+    xc.color.green = ((c & 0x0000FF00));
+    xc.color.blue  = ((c & 0x000000FF) << 8);
+    xc.color.alpha = alpha;
+    XftColorAllocValue(X.display, X.visual, X.colormap, &xc.color, &xc);
+    return xc;
+}
+
+static void redraw(void) {
+    int fheight = X.font->height;
+    int fwidth  = X.font->max_advance_width;
+    /* Allocate the colors */
+    XftColor bkgclr = xftcolor(CLR_BASE03, UINT16_MAX);
+    XftColor gtrclr = xftcolor(CLR_BASE02, UINT16_MAX);
+    XftColor csrclr = xftcolor(CLR_BASE3,  UINT16_MAX);
+    XftColor txtclr = xftcolor(CLR_BASE0,  UINT16_MAX);
+
+    /* draw the background color */
+    XftDrawRect(X.xft, &bkgclr, 0, 0, X.width, X.height);
+    /* draw the status background */
+    XftDrawRect(X.xft, &gtrclr, 0, 0, X.width, fheight);
+
+    /* Scroll the view until the cursor is visible */
+    if (CursorPos > LastDrawnPos)
+        StartRow = buf_byline(&Buffer, StartRow, 1);
+    else if (CursorPos < StartRow)
+        StartRow = buf_byline(&Buffer, StartRow, -1);
+
+    /* Draw document text */
+    int x = 0, y = 2;
+    for (LastDrawnPos = StartRow; LastDrawnPos < buf_end(&Buffer); LastDrawnPos++) {
+        if (x * fwidth >= X.width)
+            y++, x = 0;
+        if (y * fheight >= X.height)
+            break;
+        FcChar32 ch = (FcChar32)buf_get(&Buffer, LastDrawnPos);
+        if (ch == '\n') { y++, x = 0;    continue; }
+        if (ch == '\t') { x += TabWidth; continue; }
+        if (LastDrawnPos == CursorPos) {
+            XftDrawRect(X.xft, &csrclr, x * fwidth, ((y-1) * fheight) + 2, fwidth, fheight);
+            XftDrawString32(X.xft, &bkgclr, X.font, x * fwidth, y * fheight, (FcChar32 *)&ch, 1);
+        } else {
+            XftDrawString32(X.xft, &txtclr, X.font, x * fwidth, y * fheight, (FcChar32 *)&ch, 1);
+        }
+        x++;
+    }
+
+    /* flush the pixels to the screen */
+    XCopyArea(X.display, X.pixmap, X.window, X.gc, 0, 0, X.width, X.height, 0, 0);
+    XFlush(X.display);
+}
+
+int main(int argc, char** argv) {
+    init();
+    buf_init(&Buffer);
+    if (argc > 1)
+        buf_load(&Buffer, argv[1]);
+    XEvent e;
+    while (true) {
+        XPeekEvent(X.display,&e);
+        while (XPending(X.display)) {
+            XNextEvent(X.display, &e);
+            if (!XFilterEvent(&e, None))
+                handle_event(&e);
+        }
+        redraw();
+    }
+    deinit();
+    return 0;
+}