Factor out line editing to edit.c

master
Curtis McEnroe 2018-08-08 16:59:26 -04:00
parent b66c8abf70
commit eac0f83efa
No known key found for this signature in database
GPG Key ID: CEA2F97ADCFCD77C
7 changed files with 254 additions and 179 deletions

View File

@ -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

3
README
View File

@ -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

14
chat.h
View File

@ -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);

193
edit.c 100644
View File

@ -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;
}

2
pls.c
View File

@ -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;

6
tab.c
View File

@ -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);

211
ui.c
View File

@ -35,6 +35,7 @@
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#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 y, x;
getyx(ui.input, y, x);
int _, x;
getyx(ui.input, _, 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);
}