Factor out line editing to edit.c
parent
b66c8abf70
commit
eac0f83efa
2
Makefile
2
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
|
||||
|
||||
|
|
3
README
3
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
|
||||
|
|
14
chat.h
14
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);
|
||||
|
||||
|
|
|
@ -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
2
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;
|
||||
|
|
6
tab.c
6
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);
|
||||
|
|
213
ui.c
213
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 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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue