about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--README3
-rw-r--r--chat.h14
-rw-r--r--edit.c193
-rw-r--r--pls.c2
-rw-r--r--tab.c6
-rw-r--r--ui.c213
7 files changed, 254 insertions, 179 deletions
diff --git a/Makefile b/Makefile
index 38c14bf..de85e55 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@ CFLAGS += -Wall -Wextra -Wpedantic
 CFLAGS += -I/usr/local/include
 LDFLAGS += -L/usr/local/lib
 LDLIBS = -lcursesw -ltls
-OBJS = chat.o handle.o input.o irc.o pls.o tab.o ui.o
+OBJS = chat.o edit.o handle.o input.o irc.o pls.o tab.o ui.o
 
 all: tags chat
 
diff --git a/README b/README
index 59079c1..db03f11 100644
--- a/README
+++ b/README
@@ -4,7 +4,8 @@ This software targets FreeBSD and requires LibreSSL.
 
 	chat.h      Shared state and function prototypes
 	chat.c      Command line parsing and poll loop
-	ui.c        Curses UI, mIRC formatting, line editing
+	ui.c        Curses UI and mIRC formatting
+	edit.c      Line editing
 	irc.c       TLS client connection
 	input.c     Input command handling
 	handle.c    Incoming command handling
diff --git a/chat.h b/chat.h
index 09f0d2c..9507f12 100644
--- a/chat.h
+++ b/chat.h
@@ -18,6 +18,7 @@
 
 #include <stdarg.h>
 #include <stdbool.h>
+#include <stdio.h>
 #include <stdlib.h>
 #include <wchar.h>
 
@@ -31,6 +32,15 @@ struct {
 	char *chan;
 } chat;
 
+enum {
+	IRC_BOLD      = 002,
+	IRC_COLOR     = 003,
+	IRC_REVERSE   = 026,
+	IRC_RESET     = 017,
+	IRC_ITALIC    = 035,
+	IRC_UNDERLINE = 037,
+};
+
 int ircConnect(const char *host, const char *port, const char *webPass);
 void ircRead(void);
 void ircWrite(const char *ptr, size_t len);
@@ -58,6 +68,10 @@ void uiFmt(const wchar_t *format, ...);
 } while(0)
 #endif
 
+const wchar_t *editHead(void);
+const wchar_t *editTail(void);
+bool edit(bool meta, bool ctrl, wchar_t ch);
+
 void handle(char *line);
 void input(char *line);
 
diff --git a/edit.c b/edit.c
new file mode 100644
index 0000000..9073f92
--- /dev/null
+++ b/edit.c
@@ -0,0 +1,193 @@
+/* Copyright (C) 2018  Curtis McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <sysexits.h>
+#include <wchar.h>
+#include <wctype.h>
+
+#include "chat.h"
+
+enum { BUF_LEN = 512 };
+static struct {
+	wchar_t buf[BUF_LEN];
+	wchar_t *ptr;
+	wchar_t *end;
+	wchar_t *tab;
+} line = {
+	.ptr = line.buf,
+	.end = line.buf,
+};
+
+static wchar_t tail;
+const wchar_t *editHead(void) {
+	tail = *line.ptr;
+	*line.ptr = L'\0';
+	return line.buf;
+}
+const wchar_t *editTail(void) {
+	*line.ptr = tail;
+	*line.end = L'\0';
+	return line.ptr;
+}
+
+static void left(void) {
+	if (line.ptr > line.buf) line.ptr--;
+}
+static void right(void) {
+	if (line.ptr < line.end) line.ptr++;
+}
+static void home(void) {
+	line.ptr = line.buf;
+}
+static void end(void) {
+	line.ptr = line.end;
+}
+static void kill(void) {
+	line.end = line.ptr;
+}
+
+static void backspace(void) {
+	if (line.ptr == line.buf) return;
+	if (line.ptr != line.end) {
+		wmemmove(line.ptr - 1, line.ptr, line.end - line.ptr);
+	}
+	line.ptr--;
+	line.end--;
+}
+static void delete(void) {
+	if (line.ptr == line.end) return;
+	right();
+	backspace();
+}
+
+static void insert(wchar_t ch) {
+	if (line.end == &line.buf[BUF_LEN - 1]) return;
+	if (line.ptr != line.end) {
+		wmemmove(line.ptr + 1, line.ptr, line.end - line.ptr);
+	}
+	*line.ptr++ = ch;
+	line.end++;
+}
+
+static void enter(void) {
+	if (line.end == line.buf) return;
+	*line.end = L'\0';
+	char *str = awcstombs(line.buf);
+	if (!str) err(EX_DATAERR, "awcstombs");
+	input(str);
+	free(str);
+	line.ptr = line.buf;
+	line.end = line.buf;
+}
+
+static char *prefix;
+static void complete(void) {
+	if (!line.tab) {
+		editHead();
+		line.tab = wcsrchr(line.buf, L' ');
+		line.tab = (line.tab ? &line.tab[1] : line.buf);
+		prefix = awcstombs(line.tab);
+		if (!prefix) err(EX_DATAERR, "awcstombs");
+		editTail();
+	}
+
+	const char *next = tabNext(prefix);
+	if (!next) return;
+
+	wchar_t *wcs = ambstowcs(next);
+	if (!wcs) err(EX_DATAERR, "ambstowcs");
+
+	size_t i = 0;
+	for (; wcs[i] && line.ptr > &line.tab[i]; ++i) {
+		line.tab[i] = wcs[i];
+	}
+	while (line.ptr > &line.tab[i]) {
+		backspace();
+	}
+	for (; wcs[i]; ++i) {
+		insert(wcs[i]);
+	}
+	free(wcs);
+
+	size_t pos = line.tab - line.buf;
+	if (!pos) {
+		insert(L':');
+	} else if (pos >= 2) {
+		if (line.buf[pos - 2] == L':' || line.buf[pos - 2] == L',') {
+			line.buf[pos - 2] = L',';
+			insert(L':');
+		}
+	}
+	insert(L' ');
+}
+
+static void accept(void) {
+	if (!line.tab) return;
+	line.tab = NULL;
+	free(prefix);
+	tabAccept();
+}
+static void reject(void) {
+	if (!line.tab) return;
+	line.tab = NULL;
+	free(prefix);
+	tabReject();
+}
+
+static bool editMeta(wchar_t ch) {
+	(void)ch;
+	return false;
+}
+
+static bool editCtrl(wchar_t ch) {
+	switch (ch) {
+		break; case L'B': reject(); left();
+		break; case L'F': reject(); right();
+		break; case L'A': reject(); home();
+		break; case L'E': reject(); end();
+		break; case L'D': reject(); delete();
+		break; case L'K': reject(); kill();
+
+		break; case L'C': accept(); insert(IRC_COLOR);
+		break; case L'N': accept(); insert(IRC_RESET);
+		break; case L'O': accept(); insert(IRC_BOLD);
+		break; case L'R': accept(); insert(IRC_COLOR);
+		break; case L'T': accept(); insert(IRC_ITALIC);
+		break; case L'V': accept(); insert(IRC_REVERSE);
+
+		break; default: return false;
+	}
+	return true;
+}
+
+bool edit(bool meta, bool ctrl, wchar_t ch) {
+	if (meta) return editMeta(ch);
+	if (ctrl) return editCtrl(ch);
+	switch (ch) {
+		break; case L'\t': complete();
+		break; case L'\b': reject(); backspace();
+		break; case L'\n': accept(); enter();
+		break; default: {
+			if (!iswprint(ch)) return false;
+			accept();
+			insert(ch);
+		}
+	}
+	return true;
+}
diff --git a/pls.c b/pls.c
index 27ede4c..01df654 100644
--- a/pls.c
+++ b/pls.c
@@ -20,6 +20,8 @@
 #include <stdlib.h>
 #include <wchar.h>
 
+#include "chat.h"
+
 wchar_t *ambstowcs(const char *src) {
 	size_t len = mbsrtowcs(NULL, &src, 0, NULL);
 	if (len == (size_t)-1) return NULL;
diff --git a/tab.c b/tab.c
index 45670ad..a9ddfe5 100644
--- a/tab.c
+++ b/tab.c
@@ -34,7 +34,7 @@ static void prepend(struct Entry *entry) {
 	head = entry;
 }
 
-static void remove(struct Entry *entry) {
+static void unlink(struct Entry *entry) {
 	if (entry->prev) entry->prev->next = entry->next;
 	if (entry->next) entry->next->prev = entry->prev;
 	if (head == entry) head = entry->next;
@@ -42,7 +42,7 @@ static void remove(struct Entry *entry) {
 
 static void touch(struct Entry *entry) {
 	if (head == entry) return;
-	remove(entry);
+	unlink(entry);
 	prepend(entry);
 }
 
@@ -70,7 +70,7 @@ static struct Entry *match;
 void tabRemove(const char *word) {
 	for (struct Entry *entry = head; entry; entry = entry->next) {
 		if (strcmp(entry->word, word)) continue;
-		remove(entry);
+		unlink(entry);
 		if (match == entry) match = entry->next;
 		free(entry->word);
 		free(entry);
diff --git a/ui.c b/ui.c
index 937fa63..72996c1 100644
--- a/ui.c
+++ b/ui.c
@@ -34,7 +34,8 @@
 #define MIN(a, b) ((a) < (b) ? (a) : (b))
 #define MAX(a, b) ((a) > (b) ? (a) : (b))
 
-#define CTRL(c) ((c) & 037)
+#define CTRL(c)   ((c) & 037)
+#define UNCTRL(c) ((c) + '@')
 
 #ifndef A_ITALIC
 #define A_ITALIC A_NORMAL
@@ -235,14 +236,6 @@ static void wordWrap(WINDOW *win, const wchar_t *str) {
 	}
 }
 
-enum {
-	IRC_BOLD      = 0x02,
-	IRC_COLOR     = 0x03,
-	IRC_REVERSE   = 0x16,
-	IRC_RESET     = 0x0F,
-	IRC_ITALIC    = 0x1D,
-	IRC_UNDERLINE = 0x1F,
-};
 static const wchar_t IRC_CODES[] = {
 	L' ',
 	IRC_BOLD,
@@ -324,199 +317,71 @@ static void logDown(void) {
 	ui.scroll = MIN(ui.scroll + logHeight() / 2, LOG_LINES);
 }
 
-enum { BUF_LEN = 512 };
-static struct {
-	wchar_t buf[BUF_LEN];
-	wchar_t *ptr;
-	wchar_t *end;
-} line = { .ptr = line.buf, .end = line.buf };
-
-static void left(void) {
-	if (line.ptr > line.buf) line.ptr--;
-}
-static void right(void) {
-	if (line.ptr < line.end) line.ptr++;
-}
-static void home(void) {
-	line.ptr = line.buf;
-}
-static void end(void) {
-	line.ptr = line.end;
-}
-
-static void kill(void) {
-	line.end = line.ptr;
-}
-
-static void insert(wchar_t ch) {
-	if (line.end == &line.buf[BUF_LEN - 1]) return;
-	if (line.ptr != line.end) {
-		wmemmove(line.ptr + 1, line.ptr, line.end - line.ptr);
-	}
-	*line.ptr++ = ch;
-	line.end++;
-}
-
-static void backspace(void) {
-	if (line.ptr == line.buf) return;
-	if (line.ptr != line.end) {
-		wmemmove(line.ptr - 1, line.ptr, line.end - line.ptr);
-	}
-	line.ptr--;
-	line.end--;
-}
-
-static void delete(void) {
-	if (line.ptr == line.end) return;
-	right();
-	backspace();
-}
-
-static void enter(void) {
-	if (line.end == line.buf) return;
-	*line.end = L'\0';
-	char *str = awcstombs(line.buf);
-	if (!str) err(EX_DATAERR, "awcstombs");
-	input(str);
-	free(str);
-	line.ptr = line.buf;
-	line.end = line.buf;
-}
-
-static struct {
-	wchar_t *word;
-	char *prefix;
-} tab;
-
-static void accept(void) {
-	if (!tab.word) return;
-	tab.word = NULL;
-	free(tab.prefix);
-	tabAccept();
-}
-
-static void reject(void) {
-	if (!tab.word) return;
-	tab.word = NULL;
-	free(tab.prefix);
-	tabReject();
-}
-
-static void complete(void) {
-	if (!tab.word) {
-		wchar_t ch = *line.ptr;
-		*line.ptr = L'\0';
-		tab.word = wcsrchr(line.buf, L' ');
-		tab.word = (tab.word ? &tab.word[1] : line.buf);
-		tab.prefix = awcstombs(tab.word);
-		if (!tab.prefix) err(EX_DATAERR, "awcstombs");
-		*line.ptr = ch;
-	}
-
-	const char *complete = tabNext(tab.prefix);
-	if (!complete) {
-		reject();
-		return;
-	}
-
-	wchar_t *wcs = ambstowcs(complete);
-	if (!wcs) err(EX_DATAERR, "ambstowcs");
-
-	size_t i;
-	for (i = 0; wcs[i] && line.ptr > &tab.word[i]; ++i) {
-		tab.word[i] = wcs[i];
-	}
-	while (line.ptr > &tab.word[i]) {
-		backspace();
-	}
-	for (; wcs[i]; ++i) {
-		insert(wcs[i]);
-	}
-	free(wcs);
-
-	if (tab.word == line.buf) insert(L':');
-	insert(L' ');
-}
-
-static void keyChar(wint_t ch) {
+static bool keyChar(wint_t ch) {
 	static bool esc, csi;
-
-	if (csi) {
-		csi = false;
-		if (ch == L'O') logMark();
-		return;
-	}
-	csi = (esc && ch == L'[');
-	esc = (ch == L'\33');
-	if (csi) return;
-
+	bool update = false;
 	switch (ch) {
 		break; case CTRL('L'): uiRedraw();
-		break; case CTRL('B'): reject(); left();
-		break; case CTRL('F'): reject(); right();
-		break; case CTRL('A'): reject(); home();
-		break; case CTRL('E'): reject(); end();
-		break; case CTRL('D'): reject(); delete();
-		break; case CTRL('K'): reject(); kill();
-		break; case L'\b':     reject(); backspace();
-		break; case L'\177':   reject(); backspace();
-		break; case L'\t':     complete();
-		break; case L'\n':     accept(); enter();
-		break; case CTRL('C'): accept(); insert(IRC_COLOR);
-		break; case CTRL('N'): accept(); insert(IRC_RESET);
-		break; case CTRL('O'): accept(); insert(IRC_BOLD);
-		break; case CTRL('R'): accept(); insert(IRC_COLOR);
-		break; case CTRL('T'): accept(); insert(IRC_ITALIC);
-		break; case CTRL('U'): accept(); insert(IRC_UNDERLINE);
-		break; case CTRL('V'): accept(); insert(IRC_REVERSE);
+		break; case CTRL('['): esc = true; return false;
+		break; case L'\b':     update = edit(esc, false, L'\b');
+		break; case L'\177':   update = edit(esc, false, L'\b');
+		break; case L'\t':     update = edit(esc, false, L'\t');
+		break; case L'\n':     update = edit(esc, false, L'\n');
 		break; default: {
-			if (iswprint(ch)) {
-				accept();
-				insert(ch);
+			if (esc && ch == L'[') {
+				csi = true;
+				return false;
+			} else if (csi) {
+				if (ch == L'O') logMark();
+			} else if (iswcntrl(ch)) {
+				update = edit(esc, true, UNCTRL(ch));
+			} else {
+				update = edit(esc, false, ch);
 			}
 		}
 	}
+	esc = false;
+	csi = false;
+	return update;
 }
 
-static void keyCode(wint_t ch) {
+static bool keyCode(wint_t ch) {
 	switch (ch) {
 		break; case KEY_RESIZE:    uiResize();
 		break; case KEY_PPAGE:     logUp();
 		break; case KEY_NPAGE:     logDown();
-		break; case KEY_LEFT:      left();
-		break; case KEY_RIGHT:     right();
-		break; case KEY_HOME:      home();
-		break; case KEY_END:       end();
-		break; case KEY_BACKSPACE: backspace();
-		break; case KEY_DC:        delete();
-		break; case KEY_ENTER:     enter();
+		break; case KEY_LEFT:      return edit(false, true, 'B');
+		break; case KEY_RIGHT:     return edit(false, true, 'F');
+		break; case KEY_HOME:      return edit(false, true, 'A');
+		break; case KEY_END:       return edit(false, true, 'E');
+		break; case KEY_DC:        return edit(false, true, 'D');
+		break; case KEY_BACKSPACE: return edit(false, false, '\b');
+		break; case KEY_ENTER:     return edit(false, false, '\n');
 	}
+	return false;
 }
 
 void uiRead(void) {
+	bool update = false;
 	int ret;
 	wint_t ch;
 	while (ERR != (ret = wget_wch(ui.input, &ch))) {
 		if (ret == KEY_CODE_YES) {
-			keyCode(ch);
+			update |= keyCode(ch);
 		} else {
-			keyChar(ch);
+			update |= keyChar(ch);
 		}
 	}
+	if (!update) return;
 
 	wmove(ui.input, 1, 0);
+	addIRC(ui.input, editHead());
 
-	ch = *line.ptr;
-	*line.ptr = L'\0';
-	addIRC(ui.input, line.buf);
-	*line.ptr = ch;
-
-	int _, x;
-	getyx(ui.input, _, x);
+	int y, x;
+	getyx(ui.input, y, x);
 
-	*line.end = L'\0';
-	addIRC(ui.input, line.ptr);
+	addIRC(ui.input, editTail());
 
 	wclrtoeol(ui.input);
-	wmove(ui.input, 1, x);
+	wmove(ui.input, y, x);
 }