Rework UI code for multi-channel

Tags are now permanently assigned (and I'm betting on never needing more
than 256 of them) and the UI maps tags to a linked list of views for
easy reordering and removal. Currently, views can only be added. Views
don't have a topic window until they need one. All UI code wants to be
functional reactive.

Beeping is temporarily removed until message priorities (status,
message, ping) can be added to the UI. At that point spawning
notify-send should also be possible. Priorities will also help with
unnecessary markers, which will not appear for status messages.

The tab system is now used to send QUIT and NICK messages to all the
relevant tags. Verbose output now goes to its own tag, and sending to
it sends raw IRC.

IRC colors are now listed in chat.h and handler functions for numeric
replies have real names. The color algorithm now uses a real hash
function for hopefully better results. QUIT, PART and KICK messages are
scanned for URLs.
master
Curtis McEnroe 2018-08-11 19:30:30 -04:00
parent 461bfca378
commit a281f89592
No known key found for this signature in database
GPG Key ID: CEA2F97ADCFCD77C
8 changed files with 334 additions and 310 deletions

12
chat.c
View File

@ -60,7 +60,7 @@ static union {
void spawn(char *const argv[]) { void spawn(char *const argv[]) {
if (fds.pipe.events) { if (fds.pipe.events) {
uiLog(TAG_DEFAULT, L"spawn: existing pipe"); uiLog(TAG_STATUS, L"spawn: existing pipe");
return; return;
} }
@ -93,7 +93,7 @@ static void pipeRead(void) {
if (len) { if (len) {
buf[len] = '\0'; buf[len] = '\0';
len = strcspn(buf, "\n"); len = strcspn(buf, "\n");
uiFmt(TAG_DEFAULT, "%.*s", (int)len, buf); uiFmt(TAG_STATUS, "spawn: %.*s", (int)len, buf);
} else { } else {
close(fds.pipe.fd); close(fds.pipe.fd);
fds.pipe.events = 0; fds.pipe.events = 0;
@ -124,15 +124,15 @@ static void sigchld(int sig) {
pid_t pid = wait(&status); pid_t pid = wait(&status);
if (pid < 0) err(EX_OSERR, "wait"); if (pid < 0) err(EX_OSERR, "wait");
if (WIFEXITED(status) && WEXITSTATUS(status)) { if (WIFEXITED(status) && WEXITSTATUS(status)) {
uiFmt(TAG_DEFAULT, "spawn: exit %d", WEXITSTATUS(status)); uiFmt(TAG_STATUS, "spawn: exit %d", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) { } else if (WIFSIGNALED(status)) {
uiFmt(TAG_DEFAULT, "spawn: signal %d", WTERMSIG(status)); uiFmt(TAG_STATUS, "spawn: signal %d", WTERMSIG(status));
} }
} }
static void sigint(int sig) { static void sigint(int sig) {
(void)sig; (void)sig;
input(TAG_DEFAULT, "/quit"); input(TAG_STATUS, "/quit");
uiExit(); uiExit();
exit(EX_OK); exit(EX_OK);
} }
@ -182,7 +182,7 @@ int main(int argc, char *argv[]) {
inputTab(); inputTab();
uiInit(); uiInit();
uiLog(TAG_DEFAULT, L"Traveling..."); uiLog(TAG_STATUS, L"Traveling...");
uiDraw(); uiDraw();
fds.irc.fd = ircConnect(host, port, pass, webirc); fds.irc.fd = ircConnect(host, port, pass, webirc);

32
chat.h
View File

@ -42,12 +42,29 @@ struct Tag {
}; };
enum { TAGS_LEN = 256 }; enum { TAGS_LEN = 256 };
const struct Tag TAG_ALL; const struct Tag TAG_NONE;
const struct Tag TAG_DEFAULT; const struct Tag TAG_STATUS;
const struct Tag TAG_VERBOSE;
struct Tag tagFor(const char *name); struct Tag tagFor(const char *name);
struct Tag tagName(const char *name);
struct Tag tagNum(size_t num);
enum {
IRC_WHITE,
IRC_BLACK,
IRC_BLUE,
IRC_GREEN,
IRC_RED,
IRC_BROWN,
IRC_MAGENTA,
IRC_ORANGE,
IRC_YELLOW,
IRC_LIGHT_GREEN,
IRC_CYAN,
IRC_LIGHT_CYAN,
IRC_LIGHT_BLUE,
IRC_PINK,
IRC_GRAY,
IRC_LIGHT_GRAY,
};
enum { enum {
IRC_BOLD = 002, IRC_BOLD = 002,
IRC_COLOR = 003, IRC_COLOR = 003,
@ -72,9 +89,9 @@ void uiInit(void);
void uiHide(void); void uiHide(void);
void uiExit(void); void uiExit(void);
void uiDraw(void); void uiDraw(void);
void uiBeep(void);
void uiRead(void); void uiRead(void);
void uiFocus(struct Tag tag); void uiViewTag(struct Tag tag);
void uiViewNum(int num);
void uiTopic(struct Tag tag, const char *topic); void uiTopic(struct Tag tag, const char *topic);
void uiLog(struct Tag tag, const wchar_t *line); void uiLog(struct Tag tag, const wchar_t *line);
void uiFmt(struct Tag tag, const wchar_t *format, ...); void uiFmt(struct Tag tag, const wchar_t *format, ...);
@ -115,8 +132,9 @@ const wchar_t *editTail(void);
void tabTouch(struct Tag tag, const char *word); void tabTouch(struct Tag tag, const char *word);
void tabRemove(struct Tag tag, const char *word); void tabRemove(struct Tag tag, const char *word);
void tabReplace(struct Tag tag, const char *prev, const char *next);
void tabClear(struct Tag tag); void tabClear(struct Tag tag);
void tabReplace(const char *prev, const char *next); struct Tag tabTag(const char *word);
const char *tabNext(struct Tag tag, const char *prefix); const char *tabNext(struct Tag tag, const char *prefix);
void tabAccept(void); void tabAccept(void);
void tabReject(void); void tabReject(void);

115
handle.c
View File

@ -23,14 +23,17 @@
#include "chat.h" #include "chat.h"
static int color(const char *s) { // Adapted from <https://github.com/cbreeden/fxhash/blob/master/lib.rs>.
if (!s) return 0; static int color(const char *str) {
int x = 0; if (!str) return IRC_GRAY;
for (; s[0]; ++s) { uint32_t hash = 0;
x ^= s[0]; for (; str[0]; ++str) {
hash = (hash << 5) | (hash >> 27);
hash ^= str[0];
hash *= 0x27220A95;
} }
x &= 15; hash &= IRC_LIGHT_GRAY;
return (x == 1) ? 0 : x; return (hash == IRC_BLACK) ? IRC_GRAY : hash;
} }
static char *paramField(char **params) { static char *paramField(char **params) {
@ -90,44 +93,41 @@ static void handlePing(char *prefix, char *params) {
ircFmt("PONG %s\r\n", params); ircFmt("PONG %s\r\n", params);
} }
static void handle432(char *prefix, char *params) { static void handleReplyErroneousNickname(char *prefix, char *params) {
char *mesg; char *mesg;
shift(prefix, NULL, NULL, NULL, params, 3, 0, NULL, NULL, &mesg); shift(prefix, NULL, NULL, NULL, params, 3, 0, NULL, NULL, &mesg);
uiLog(TAG_DEFAULT, L"You can't use that name here"); uiLog(TAG_STATUS, L"You can't use that name here");
uiFmt(TAG_DEFAULT, "Sheriff says, \"%s\"", mesg); uiFmt(TAG_STATUS, "Sheriff says, \"%s\"", mesg);
uiLog(TAG_DEFAULT, L"Type /nick <name> to choose a new one"); uiLog(TAG_STATUS, L"Type /nick <name> to choose a new one");
} }
static void handle001(char *prefix, char *params) { static void handleReplyWelcome(char *prefix, char *params) {
char *nick; char *nick;
shift(prefix, NULL, NULL, NULL, params, 1, 0, &nick); shift(prefix, NULL, NULL, NULL, params, 1, 0, &nick);
if (strcmp(nick, self.nick)) selfNick(nick); if (strcmp(nick, self.nick)) selfNick(nick);
tabTouch(TAG_DEFAULT, self.nick); tabTouch(TAG_STATUS, self.nick);
if (self.join) ircFmt("JOIN %s\r\n", self.join); if (self.join) ircFmt("JOIN %s\r\n", self.join);
uiLog(TAG_DEFAULT, L"You have arrived"); uiLog(TAG_STATUS, L"You have arrived");
} }
static void handle372(char *prefix, char *params) { static void handleReplyMOTD(char *prefix, char *params) {
char *mesg; char *mesg;
shift(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &mesg); shift(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &mesg);
if (mesg[0] == '-' && mesg[1] == ' ') mesg = &mesg[2]; if (mesg[0] == '-' && mesg[1] == ' ') mesg = &mesg[2];
uiFmt(TAG_DEFAULT, "%s", mesg); urlScan(TAG_STATUS, mesg);
uiFmt(TAG_STATUS, "%s", mesg);
} }
static void handleJoin(char *prefix, char *params) { static void handleJoin(char *prefix, char *params) {
char *nick, *user, *chan; char *nick, *user, *chan;
shift(prefix, &nick, &user, NULL, params, 1, 0, &chan); shift(prefix, &nick, &user, NULL, params, 1, 0, &chan);
struct Tag tag = tagFor(chan); struct Tag tag = tagFor(chan);
if (isSelf(nick, user)) { tabTouch(tag, nick);
tabTouch(TAG_DEFAULT, chan);
uiFocus(tag);
} else {
tabTouch(tag, nick);
}
uiFmt( uiFmt(
tag, "\3%d%s\3 arrives in \3%d%s\3", tag, "\3%d%s\3 arrives in \3%d%s\3",
color(user), nick, color(chan), chan color(user), nick, color(chan), chan
); );
if (isSelf(nick, user)) uiViewTag(tag);
} }
static void handlePart(char *prefix, char *params) { static void handlePart(char *prefix, char *params) {
@ -136,6 +136,7 @@ static void handlePart(char *prefix, char *params) {
struct Tag tag = tagFor(chan); struct Tag tag = tagFor(chan);
(void)(isSelf(nick, user) ? tabClear(tag) : tabRemove(tag, nick)); (void)(isSelf(nick, user) ? tabClear(tag) : tabRemove(tag, nick));
if (mesg) { if (mesg) {
urlScan(tag, mesg);
uiFmt( uiFmt(
tag, "\3%d%s\3 leaves \3%d%s\3, \"%s\"", tag, "\3%d%s\3 leaves \3%d%s\3, \"%s\"",
color(user), nick, color(chan), chan, mesg color(user), nick, color(chan), chan, mesg
@ -154,6 +155,7 @@ static void handleKick(char *prefix, char *params) {
struct Tag tag = tagFor(chan); struct Tag tag = tagFor(chan);
(void)(isSelf(nick, user) ? tabClear(tag) : tabRemove(tag, nick)); (void)(isSelf(nick, user) ? tabClear(tag) : tabRemove(tag, nick));
if (mesg) { if (mesg) {
urlScan(tag, mesg);
uiFmt( uiFmt(
tag, "\3%d%s\3 kicks \3%d%s\3 out of \3%d%s\3, \"%s\"", tag, "\3%d%s\3 kicks \3%d%s\3 out of \3%d%s\3, \"%s\"",
color(user), nick, color(kick), kick, color(chan), chan, mesg color(user), nick, color(kick), kick, color(chan), chan, mesg
@ -169,20 +171,23 @@ static void handleKick(char *prefix, char *params) {
static void handleQuit(char *prefix, char *params) { static void handleQuit(char *prefix, char *params) {
char *nick, *user, *mesg; char *nick, *user, *mesg;
shift(prefix, &nick, &user, NULL, params, 0, 1, &mesg); shift(prefix, &nick, &user, NULL, params, 0, 1, &mesg);
// TODO: Send to tags where nick is in tab. char *quot = (mesg && mesg[0] == '"') ? "" : "\"";
tabRemove(TAG_ALL, nick); struct Tag tag;
if (mesg) { while (TAG_NONE.id != (tag = tabTag(nick)).id) {
char *quot = (mesg[0] == '"') ? "" : "\""; tabRemove(tag, nick);
uiFmt( if (mesg) {
TAG_DEFAULT, "\3%d%s\3 leaves, %s%s%s", urlScan(tag, mesg);
color(user), nick, quot, mesg, quot uiFmt(
); tag, "\3%d%s\3 leaves, %s%s%s",
} else { color(user), nick, quot, mesg, quot
uiFmt(TAG_DEFAULT, "\3%d%s\3 leaves", color(user), nick); );
} else {
uiFmt(tag, "\3%d%s\3 leaves", color(user), nick);
}
} }
} }
static void handle332(char *prefix, char *params) { static void handleReplyTopic(char *prefix, char *params) {
char *chan, *topic; char *chan, *topic;
shift(prefix, NULL, NULL, NULL, params, 3, 0, NULL, &chan, &topic); shift(prefix, NULL, NULL, NULL, params, 3, 0, NULL, &chan, &topic);
struct Tag tag = tagFor(chan); struct Tag tag = tagFor(chan);
@ -207,7 +212,7 @@ static void handleTopic(char *prefix, char *params) {
); );
} }
static void handle366(char *prefix, char *params) { static void handleReplyEndOfNames(char *prefix, char *params) {
char *chan; char *chan;
shift(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &chan); shift(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &chan);
ircFmt("WHO %s\r\n", chan); ircFmt("WHO %s\r\n", chan);
@ -219,14 +224,14 @@ static struct {
size_t len; size_t len;
} who; } who;
static void handle352(char *prefix, char *params) { static void handleReplyWho(char *prefix, char *params) {
char *chan, *user, *nick; char *chan, *user, *nick;
shift( shift(
prefix, NULL, NULL, NULL, prefix, NULL, NULL, NULL,
params, 6, 0, NULL, &chan, &user, NULL, NULL, &nick params, 6, 0, NULL, &chan, &user, NULL, NULL, &nick
); );
struct Tag tag = tagFor(chan); struct Tag tag = tagFor(chan);
if (!isSelf(nick, user)) tabTouch(tag, nick); tabTouch(tag, nick);
size_t cap = sizeof(who.buf) - who.len; size_t cap = sizeof(who.buf) - who.len;
int len = snprintf( int len = snprintf(
&who.buf[who.len], cap, &who.buf[who.len], cap,
@ -236,7 +241,7 @@ static void handle352(char *prefix, char *params) {
if ((size_t)len < cap) who.len += len; if ((size_t)len < cap) who.len += len;
} }
static void handle315(char *prefix, char *params) { static void handleReplyEndOfWho(char *prefix, char *params) {
char *chan; char *chan;
shift(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &chan); shift(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &chan);
struct Tag tag = tagFor(chan); struct Tag tag = tagFor(chan);
@ -251,12 +256,14 @@ static void handleNick(char *prefix, char *params) {
char *prev, *user, *next; char *prev, *user, *next;
shift(prefix, &prev, &user, NULL, params, 1, 0, &next); shift(prefix, &prev, &user, NULL, params, 1, 0, &next);
if (isSelf(prev, user)) selfNick(next); if (isSelf(prev, user)) selfNick(next);
// TODO: Send to tags where prev is in tab. struct Tag tag;
tabReplace(prev, next); while (TAG_NONE.id != (tag = tabTag(prev)).id) {
uiFmt( tabReplace(tag, prev, next);
TAG_DEFAULT, "\3%d%s\3 is now known as \3%d%s\3", uiFmt(
color(user), prev, color(user), next tag, "\3%d%s\3 is now known as \3%d%s\3",
); color(user), prev, color(user), next
);
}
} }
static void handleCTCP(struct Tag tag, char *nick, char *user, char *mesg) { static void handleCTCP(struct Tag tag, char *nick, char *user, char *mesg) {
@ -288,13 +295,13 @@ static void handlePrivmsg(char *prefix, char *params) {
tag, "%c\3%d%c%s%c\17 %s", tag, "%c\3%d%c%s%c\17 %s",
ping["\17\26"], color(user), self["<("], nick, self[">)"], mesg ping["\17\26"], color(user), self["<("], nick, self[">)"], mesg
); );
if (ping) uiBeep(); // TODO: always be beeping.
} }
static void handleNotice(char *prefix, char *params) { static void handleNotice(char *prefix, char *params) {
char *nick, *user, *chan, *mesg; char *nick, *user, *chan, *mesg;
shift(prefix, &nick, &user, NULL, params, 2, 0, &chan, &mesg); shift(prefix, &nick, &user, NULL, params, 2, 0, &chan, &mesg);
struct Tag tag = TAG_DEFAULT; struct Tag tag = TAG_STATUS;
if (user) tag = (strcmp(chan, self.nick) ? tagFor(chan) : tagFor(nick)); if (user) tag = (strcmp(chan, self.nick) ? tagFor(chan) : tagFor(nick));
if (!isSelf(nick, user)) tabTouch(tag, nick); if (!isSelf(nick, user)) tabTouch(tag, nick);
urlScan(tag, mesg); urlScan(tag, mesg);
@ -308,15 +315,15 @@ static const struct {
const char *command; const char *command;
Handler handler; Handler handler;
} HANDLERS[] = { } HANDLERS[] = {
{ "001", handle001 }, { "001", handleReplyWelcome },
{ "315", handle315 }, { "315", handleReplyEndOfWho },
{ "332", handle332 }, { "332", handleReplyTopic },
{ "352", handle352 }, { "352", handleReplyWho },
{ "366", handle366 }, { "366", handleReplyEndOfNames },
{ "372", handle372 }, { "372", handleReplyMOTD },
{ "375", handle372 }, { "375", handleReplyMOTD },
{ "432", handle432 }, { "432", handleReplyErroneousNickname },
{ "433", handle432 }, { "433", handleReplyErroneousNickname },
{ "JOIN", handleJoin }, { "JOIN", handleJoin },
{ "KICK", handleKick }, { "KICK", handleKick },
{ "NICK", handleNick }, { "NICK", handleNick },

24
input.c
View File

@ -24,7 +24,6 @@
#include "chat.h" #include "chat.h"
static void privmsg(struct Tag tag, bool action, const char *mesg) { static void privmsg(struct Tag tag, bool action, const char *mesg) {
if (tag.id == TAG_DEFAULT.id) return;
char *line; char *line;
int send; int send;
asprintf( asprintf(
@ -50,7 +49,7 @@ static void inputNick(struct Tag tag, char *params) {
if (nick) { if (nick) {
ircFmt("NICK %s\r\n", nick); ircFmt("NICK %s\r\n", nick);
} else { } else {
uiLog(TAG_DEFAULT, L"/nick requires a name"); uiLog(TAG_STATUS, L"/nick requires a name");
} }
} }
@ -60,7 +59,7 @@ static void inputJoin(struct Tag tag, char *params) {
if (chan) { if (chan) {
ircFmt("JOIN %s\r\n", chan); ircFmt("JOIN %s\r\n", chan);
} else { } else {
uiLog(TAG_DEFAULT, L"/join requires a channel"); uiLog(TAG_STATUS, L"/join requires a channel");
} }
} }
@ -104,9 +103,12 @@ static void inputOpen(struct Tag tag, char *params) {
static void inputView(struct Tag tag, char *params) { static void inputView(struct Tag tag, char *params) {
char *view = strsep(&params, " "); char *view = strsep(&params, " ");
if (!view) return; if (!view) return;
size_t num = strtoul(view, &view, 0); int num = strtol(view, &view, 0);
tag = (view[0] ? tagName(view) : tagNum(num)); if (view[0]) {
if (tag.name) uiFocus(tag); uiViewTag(tagFor(view));
} else {
uiViewNum(num);
}
} }
static const struct { static const struct {
@ -128,7 +130,11 @@ static const size_t COMMANDS_LEN = sizeof(COMMANDS) / sizeof(COMMANDS[0]);
void input(struct Tag tag, char *input) { void input(struct Tag tag, char *input) {
if (input[0] != '/') { if (input[0] != '/') {
privmsg(tag, false, input); if (tag.id == TAG_VERBOSE.id) {
ircFmt("%s\r\n", input);
} else if (tag.id != TAG_STATUS.id) {
privmsg(tag, false, input);
}
return; return;
} }
char *command = strsep(&input, " "); char *command = strsep(&input, " ");
@ -138,11 +144,11 @@ void input(struct Tag tag, char *input) {
COMMANDS[i].handler(tag, input); COMMANDS[i].handler(tag, input);
return; return;
} }
uiFmt(TAG_DEFAULT, "%s isn't a recognized command", command); uiFmt(TAG_STATUS, "%s isn't a recognized command", command);
} }
void inputTab(void) { void inputTab(void) {
for (size_t i = 0; i < COMMANDS_LEN; ++i) { for (size_t i = 0; i < COMMANDS_LEN; ++i) {
tabTouch(TAG_DEFAULT, COMMANDS[i].command); tabTouch(TAG_NONE, COMMANDS[i].command);
} }
} }

10
irc.c
View File

@ -106,15 +106,15 @@ void ircFmt(const char *format, ...) {
int len = vasprintf(&buf, format, ap); int len = vasprintf(&buf, format, ap);
va_end(ap); va_end(ap);
if (!buf) err(EX_OSERR, "vasprintf"); if (!buf) err(EX_OSERR, "vasprintf");
if (self.verbose) uiFmt(tagFor("(irc)"), "\00314<<<\3 %.*s", len - 2, buf); if (self.verbose) uiFmt(TAG_VERBOSE, "\3%d<<<\3 %.*s", IRC_WHITE, len - 2, buf);
ircWrite(buf, len); ircWrite(buf, len);
free(buf); free(buf);
} }
static char buf[4096];
static size_t len;
void ircRead(void) { void ircRead(void) {
static char buf[4096];
static size_t len;
ssize_t read = tls_read(client, &buf[len], sizeof(buf) - len); ssize_t read = tls_read(client, &buf[len], sizeof(buf) - len);
if (read < 0) errx(EX_IOERR, "tls_read: %s", tls_error(client)); if (read < 0) errx(EX_IOERR, "tls_read: %s", tls_error(client));
if (!read) { if (!read) {
@ -126,7 +126,7 @@ void ircRead(void) {
char *crlf, *line = buf; char *crlf, *line = buf;
while ((crlf = strnstr(line, "\r\n", &buf[len] - line))) { while ((crlf = strnstr(line, "\r\n", &buf[len] - line))) {
crlf[0] = '\0'; crlf[0] = '\0';
if (self.verbose) uiFmt(tagFor("(irc)"), "\00314>>>\3 %s", line); if (self.verbose) uiFmt(TAG_VERBOSE, "\3%d>>>\3 %s", IRC_GRAY, line);
handle(line); handle(line);
line = &crlf[2]; line = &crlf[2];
} }

55
tab.c
View File

@ -22,7 +22,7 @@
#include "chat.h" #include "chat.h"
static struct Entry { static struct Entry {
size_t tag; struct Tag tag;
char *word; char *word;
struct Entry *prev; struct Entry *prev;
struct Entry *next; struct Entry *next;
@ -49,7 +49,7 @@ static void touch(struct Entry *entry) {
void tabTouch(struct Tag tag, const char *word) { void tabTouch(struct Tag tag, const char *word) {
for (struct Entry *entry = head; entry; entry = entry->next) { for (struct Entry *entry = head; entry; entry = entry->next) {
if (entry->tag != tag.id) continue; if (entry->tag.id != tag.id) continue;
if (strcmp(entry->word, word)) continue; if (strcmp(entry->word, word)) continue;
touch(entry); touch(entry);
return; return;
@ -58,30 +58,28 @@ void tabTouch(struct Tag tag, const char *word) {
struct Entry *entry = malloc(sizeof(*entry)); struct Entry *entry = malloc(sizeof(*entry));
if (!entry) err(EX_OSERR, "malloc"); if (!entry) err(EX_OSERR, "malloc");
entry->tag = tag.id; entry->tag = tag;
entry->word = strdup(word); entry->word = strdup(word);
if (!entry->word) err(EX_OSERR, "strdup"); if (!entry->word) err(EX_OSERR, "strdup");
prepend(entry); prepend(entry);
} }
void tabReplace(const char *prev, const char *next) { void tabReplace(struct Tag tag, const char *prev, const char *next) {
for (struct Entry *entry = head; entry; entry = entry->next) { tabTouch(tag, prev);
if (strcmp(entry->word, prev)) continue; free(head->word);
free(entry->word); head->word = strdup(next);
entry->word = strdup(next); if (!head->word) err(EX_OSERR, "strdup");
if (!entry->word) err(EX_OSERR, "strdup");
}
} }
static struct Entry *match; static struct Entry *iter;
void tabRemove(struct Tag tag, const char *word) { void tabRemove(struct Tag tag, const char *word) {
for (struct Entry *entry = head; entry; entry = entry->next) { for (struct Entry *entry = head; entry; entry = entry->next) {
if (tag.id != TAG_ALL.id && entry->tag != tag.id) continue; if (entry->tag.id != tag.id) continue;
if (strcmp(entry->word, word)) continue; if (strcmp(entry->word, word)) continue;
if (iter == entry) iter = entry->prev;
unlink(entry); unlink(entry);
if (match == entry) match = entry->prev;
free(entry->word); free(entry->word);
free(entry); free(entry);
return; return;
@ -90,33 +88,44 @@ void tabRemove(struct Tag tag, const char *word) {
void tabClear(struct Tag tag) { void tabClear(struct Tag tag) {
for (struct Entry *entry = head; entry; entry = entry->next) { for (struct Entry *entry = head; entry; entry = entry->next) {
if (entry->tag != tag.id) continue; if (entry->tag.id != tag.id) continue;
if (iter == entry) iter = entry->prev;
unlink(entry); unlink(entry);
if (match == entry) match = entry->prev;
free(entry->word); free(entry->word);
free(entry); free(entry);
} }
} }
struct Tag tabTag(const char *word) {
struct Entry *start = (iter ? iter->next : head);
for (struct Entry *entry = start; entry; entry = entry->next) {
if (strcmp(entry->word, word)) continue;
iter = entry;
return entry->tag;
}
iter = NULL;
return TAG_NONE;
}
const char *tabNext(struct Tag tag, const char *prefix) { const char *tabNext(struct Tag tag, const char *prefix) {
size_t len = strlen(prefix); size_t len = strlen(prefix);
struct Entry *start = (match ? match->next : head); struct Entry *start = (iter ? iter->next : head);
for (struct Entry *entry = start; entry; entry = entry->next) { for (struct Entry *entry = start; entry; entry = entry->next) {
if (entry->tag != TAG_DEFAULT.id && entry->tag != tag.id) continue; if (entry->tag.id != TAG_NONE.id && entry->tag.id != tag.id) continue;
if (strncasecmp(entry->word, prefix, len)) continue; if (strncasecmp(entry->word, prefix, len)) continue;
match = entry; iter = entry;
return entry->word; return entry->word;
} }
if (!match) return NULL; if (!iter) return NULL;
match = NULL; iter = NULL;
return tabNext(tag, prefix); return tabNext(tag, prefix);
} }
void tabAccept(void) { void tabAccept(void) {
if (match) touch(match); if (iter) touch(iter);
match = NULL; iter = NULL;
} }
void tabReject(void) { void tabReject(void) {
match = NULL; iter = NULL;
} }

45
tag.c
View File

@ -21,57 +21,30 @@
#include "chat.h" #include "chat.h"
const struct Tag TAG_ALL = { (size_t)-1, NULL }; const struct Tag TAG_NONE = { 0, "" };
const struct Tag TAG_DEFAULT = { 0, "(status)" }; const struct Tag TAG_STATUS = { 1, "(status)" };
const struct Tag TAG_VERBOSE = { 2, "(irc)" };
static struct { static struct {
char *name[TAGS_LEN]; char *name[TAGS_LEN];
size_t len; size_t len;
size_t gap;
} tags = { } tags = {
.name = { "(status)" }, .name = { "", "(status)", "(irc)" },
.len = 1, .len = 3,
.gap = 1,
}; };
static struct Tag Tag(size_t id) { static struct Tag Tag(size_t id) {
return (struct Tag) { id, tags.name[id] }; return (struct Tag) { id, tags.name[id] };
} }
struct Tag tagName(const char *name) { struct Tag tagFor(const char *name) {
for (size_t id = 0; id < tags.len; ++id) { for (size_t id = 0; id < tags.len; ++id) {
if (!tags.name[id] || strcmp(tags.name[id], name)) continue; if (strcmp(tags.name[id], name)) continue;
return Tag(id); return Tag(id);
} }
return TAG_ALL; if (tags.len == TAGS_LEN) return TAG_STATUS;
} size_t id = tags.len++;
struct Tag tagNum(size_t num) {
if (num < tags.gap) return Tag(num);
num -= tags.gap;
for (size_t id = tags.gap; id < tags.len; ++id) {
if (!tags.name[id]) continue;
if (!num--) return Tag(id);
}
return TAG_ALL;
}
struct Tag tagFor(const char *name) {
struct Tag tag = tagName(name);
if (tag.name) return tag;
size_t id = tags.gap;
tags.name[id] = strdup(name); tags.name[id] = strdup(name);
if (!tags.name[id]) err(EX_OSERR, "strdup"); if (!tags.name[id]) err(EX_OSERR, "strdup");
if (tags.gap == tags.len) {
tags.gap++;
tags.len++;
} else {
for (tags.gap++; tags.gap < tags.len; ++tags.gap) {
if (!tags.name[tags.gap]) break;
}
}
return Tag(id); return Tag(id);
} }

351
ui.c
View File

@ -21,13 +21,16 @@
#include <locale.h> #include <locale.h>
#include <stdarg.h> #include <stdarg.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sysexits.h> #include <sysexits.h>
#include <wchar.h> #include <wchar.h>
#include <wctype.h> #include <wctype.h>
#ifndef A_ITALIC
#define A_ITALIC A_NORMAL
#endif
#include "chat.h" #include "chat.h"
#undef uiFmt #undef uiFmt
@ -35,20 +38,6 @@
#define MAX(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
#endif
static void focusEnable(void) {
printf("\33[?1004h");
fflush(stdout);
}
static void focusDisable(void) {
printf("\33[?1004l");
fflush(stdout);
}
static void colorInit(void) { static void colorInit(void) {
start_color(); start_color();
@ -81,52 +70,83 @@ static short pair8(short pair) {
return (pair & 0x70) >> 1 | (pair & 0x07); return (pair & 0x70) >> 1 | (pair & 0x07);
} }
static const int TOPIC_COLS = 512; static const int LOG_LINES = 256;
static const int INPUT_COLS = 512; static const int TOPIC_COLS = 512;
static const int LOG_LINES = 100; static const int INPUT_COLS = 512;
static struct View {
struct Tag tag;
WINDOW *topic;
WINDOW *log;
int scroll;
bool mark;
struct View *prev;
struct View *next;
} *viewHead, *viewTail;
static void viewAppend(struct View *view) {
if (viewTail) viewTail->next = view;
view->prev = viewTail;
view->next = NULL;
viewTail = view;
if (!viewHead) viewHead = view;
}
static int logHeight(const struct View *view) {
return LINES - (view->topic ? 2 : 0) - 2;
}
static int lastLogLine(void) {
return LOG_LINES - 1;
}
static int lastLine(void) { static int lastLine(void) {
return LINES - 1; return LINES - 1;
} }
static int lastCol(void) { static int lastCol(void) {
return COLS - 1; return COLS - 1;
} }
static int logHeight(void) {
return LINES - 4;
}
struct View {
WINDOW *topic;
WINDOW *log;
int scroll;
bool mark;
};
static struct { static struct {
bool hide; bool hide;
WINDOW *input; WINDOW *input;
struct Tag tag; struct View *view;
struct View views[TAGS_LEN]; struct View *tags[TAGS_LEN];
size_t len;
} ui; } ui;
static struct View *viewTag(struct Tag tag) {
struct View *view = ui.tags[tag.id];
if (view) return view;
view = calloc(1, sizeof(*view));
if (!view) err(EX_OSERR, "calloc");
view->tag = tag;
view->log = newpad(LOG_LINES, COLS);
wsetscrreg(view->log, 0, lastLogLine());
scrollok(view->log, true);
wmove(view->log, lastLogLine() - logHeight(view), 0);
view->scroll = LOG_LINES;
viewAppend(view);
ui.tags[tag.id] = view;
return view;
}
void uiInit(void) { void uiInit(void) {
setlocale(LC_CTYPE, ""); setlocale(LC_CTYPE, "");
initscr(); initscr();
cbreak(); cbreak();
noecho(); noecho();
focusEnable();
colorInit(); colorInit();
termMode(TERM_FOCUS, true);
ui.tag = TAG_DEFAULT;
ui.input = newpad(2, INPUT_COLS); ui.input = newpad(2, INPUT_COLS);
mvwhline(ui.input, 0, 0, ACS_HLINE, INPUT_COLS); mvwhline(ui.input, 0, 0, ACS_HLINE, INPUT_COLS);
wmove(ui.input, 1, 0); wmove(ui.input, 1, 0);
keypad(ui.input, true); keypad(ui.input, true);
nodelay(ui.input, true); nodelay(ui.input, true);
ui.view = viewTag(TAG_STATUS);
} }
void uiHide(void) { void uiHide(void) {
@ -136,54 +156,34 @@ void uiHide(void) {
void uiExit(void) { void uiExit(void) {
uiHide(); uiHide();
focusDisable(); termMode(TERM_FOCUS, false);
printf( printf(
"This program is AGPLv3 free software!\n" "This program is AGPLv3 free software!\n"
"The source is available at <" SOURCE_URL ">.\n" "The source is available at <" SOURCE_URL ">.\n"
); );
} }
static struct View *uiView(struct Tag tag) {
struct View *view = &ui.views[tag.id];
if (view->log) return view;
view->topic = newpad(2, TOPIC_COLS);
mvwhline(view->topic, 1, 0, ACS_HLINE, TOPIC_COLS);
view->log = newpad(LOG_LINES, COLS);
wsetscrreg(view->log, 0, LOG_LINES - 1);
scrollok(view->log, true);
wmove(view->log, LOG_LINES - logHeight() - 1, 0);
view->scroll = LOG_LINES;
view->mark = false;
if (tag.id >= ui.len) ui.len = tag.id + 1;
return view;
}
static void uiResize(void) { static void uiResize(void) {
for (size_t i = 0; i < ui.len; ++i) { for (struct View *view = viewHead; view; view = view->next) {
struct View *view = &ui.views[i];
if (!view->log) continue;
wresize(view->log, LOG_LINES, COLS); wresize(view->log, LOG_LINES, COLS);
wmove(view->log, LOG_LINES - 1, COLS - 1); wmove(view->log, lastLogLine(), lastCol());
} }
} }
void uiDraw(void) { void uiDraw(void) {
if (ui.hide) return; if (ui.hide) return;
struct View *view = uiView(ui.tag); if (ui.view->topic) {
pnoutrefresh(
ui.view->topic,
0, 0,
0, 0,
1, lastCol()
);
}
pnoutrefresh( pnoutrefresh(
view->topic, ui.view->log,
0, 0, ui.view->scroll - logHeight(ui.view), 0,
0, 0, (ui.view->topic ? 2 : 0), 0,
1, lastCol()
);
pnoutrefresh(
view->log,
view->scroll - logHeight(), 0,
2, 0,
lastLine() - 2, lastCol() lastLine() - 2, lastCol()
); );
int _, x; int _, x;
@ -201,37 +201,51 @@ static void uiRedraw(void) {
clearok(curscr, true); clearok(curscr, true);
} }
void uiFocus(struct Tag tag) { static void uiView(struct View *view) {
struct View *view = uiView(ui.tag); if (view->topic) touchwin(view->topic);
view->mark = true;
view = uiView(tag);
view->mark = false;
touchwin(view->topic);
touchwin(view->log); touchwin(view->log);
ui.tag = tag; view->mark = false;
ui.view->mark = true;
ui.view = view;
} }
void uiBeep(void) { void uiViewTag(struct Tag tag) {
beep(); // always be beeping uiView(viewTag(tag));
} }
static const short IRC_COLORS[16] = { void uiViewNum(int num) {
8 + COLOR_WHITE, // white if (num < 0) {
0 + COLOR_BLACK, // black for (struct View *view = viewTail; view; view = view->prev) {
0 + COLOR_BLUE, // blue if (++num) continue;
0 + COLOR_GREEN, // green uiView(view);
8 + COLOR_RED, // red break;
0 + COLOR_RED, // brown }
0 + COLOR_MAGENTA, // magenta } else {
0 + COLOR_YELLOW, // orange for (struct View *view = viewHead; view; view = view->next) {
8 + COLOR_YELLOW, // yellow if (num--) continue;
8 + COLOR_GREEN, // light green uiView(view);
0 + COLOR_CYAN, // cyan break;
8 + COLOR_CYAN, // light cyan }
8 + COLOR_BLUE, // light blue }
8 + COLOR_MAGENTA, // pink }
8 + COLOR_BLACK, // gray
0 + COLOR_WHITE, // light gray static const short IRC_COLORS[] = {
[IRC_WHITE] = 8 + COLOR_WHITE,
[IRC_BLACK] = 0 + COLOR_BLACK,
[IRC_BLUE] = 0 + COLOR_BLUE,
[IRC_GREEN] = 0 + COLOR_GREEN,
[IRC_RED] = 8 + COLOR_RED,
[IRC_BROWN] = 0 + COLOR_RED,
[IRC_MAGENTA] = 0 + COLOR_MAGENTA,
[IRC_ORANGE] = 0 + COLOR_YELLOW,
[IRC_YELLOW] = 8 + COLOR_YELLOW,
[IRC_LIGHT_GREEN] = 8 + COLOR_GREEN,
[IRC_CYAN] = 0 + COLOR_CYAN,
[IRC_LIGHT_CYAN] = 8 + COLOR_CYAN,
[IRC_LIGHT_BLUE] = 8 + COLOR_BLUE,
[IRC_PINK] = 8 + COLOR_MAGENTA,
[IRC_GRAY] = 8 + COLOR_BLACK,
[IRC_LIGHT_GRAY] = 0 + COLOR_WHITE,
}; };
static const wchar_t *parseColor(short *pair, const wchar_t *str) { static const wchar_t *parseColor(short *pair, const wchar_t *str) {
@ -310,9 +324,13 @@ static void addIRC(WINDOW *win, const wchar_t *str) {
} }
void uiTopic(struct Tag tag, const char *topic) { void uiTopic(struct Tag tag, const char *topic) {
struct View *view = viewTag(tag);
if (!view->topic) {
view->topic = newpad(2, TOPIC_COLS);
mvwhline(view->topic, 1, 0, ACS_HLINE, TOPIC_COLS);
}
wchar_t *wcs = ambstowcs(topic); wchar_t *wcs = ambstowcs(topic);
if (!wcs) err(EX_DATAERR, "ambstowcs"); if (!wcs) err(EX_DATAERR, "ambstowcs");
struct View *view = uiView(tag);
wmove(view->topic, 0, 0); wmove(view->topic, 0, 0);
addIRC(view->topic, wcs); addIRC(view->topic, wcs);
wclrtoeol(view->topic); wclrtoeol(view->topic);
@ -320,7 +338,7 @@ void uiTopic(struct Tag tag, const char *topic) {
} }
void uiLog(struct Tag tag, const wchar_t *line) { void uiLog(struct Tag tag, const wchar_t *line) {
struct View *view = uiView(tag); struct View *view = viewTag(tag);
waddch(view->log, '\n'); waddch(view->log, '\n');
if (view->mark) { if (view->mark) {
waddch(view->log, '\n'); waddch(view->log, '\n');
@ -330,93 +348,89 @@ void uiLog(struct Tag tag, const wchar_t *line) {
} }
void uiFmt(struct Tag tag, const wchar_t *format, ...) { void uiFmt(struct Tag tag, const wchar_t *format, ...) {
wchar_t *buf; wchar_t *wcs;
va_list ap; va_list ap;
va_start(ap, format); va_start(ap, format);
vaswprintf(&buf, format, ap); vaswprintf(&wcs, format, ap);
va_end(ap); va_end(ap);
if (!buf) err(EX_OSERR, "vaswprintf"); if (!wcs) err(EX_OSERR, "vaswprintf");
uiLog(tag, buf); uiLog(tag, wcs);
free(buf); free(wcs);
} }
static void logUp(void) { static void logPageUp(void) {
struct View *view = uiView(ui.tag); int height = logHeight(ui.view);
if (view->scroll == logHeight()) return; if (ui.view->scroll == height) return;
if (view->scroll == LOG_LINES) view->mark = true; if (ui.view->scroll == LOG_LINES) ui.view->mark = true;
view->scroll = MAX(view->scroll - logHeight() / 2, logHeight()); ui.view->scroll = MAX(ui.view->scroll - height / 2, height);
} }
static void logDown(void) { static void logPageDown(void) {
struct View *view = uiView(ui.tag); if (ui.view->scroll == LOG_LINES) return;
if (view->scroll == LOG_LINES) return; ui.view->scroll = MIN(ui.view->scroll + logHeight(ui.view) / 2, LOG_LINES);
view->scroll = MIN(view->scroll + logHeight() / 2, LOG_LINES); if (ui.view->scroll == LOG_LINES) ui.view->mark = false;
if (view->scroll == LOG_LINES) view->mark = false;
} }
static bool keyChar(wchar_t ch) { static bool keyChar(wchar_t ch) {
static bool esc, csi; if (iswascii(ch)) {
if (ch == L'\33') { enum TermEvent event = termEvent((char)ch);
esc = true; switch (event) {
return false; break; case TERM_FOCUS_IN: ui.view->mark = false;
break; case TERM_FOCUS_OUT: ui.view->mark = true;
break; default: {}
}
if (event) return false;
} }
if (esc && ch == L'[') {
esc = false;
csi = true;
return false;
}
if (csi) {
if (ch == L'O') uiView(ui.tag)->mark = true;
if (ch == L'I') uiView(ui.tag)->mark = false;
csi = false;
return false;
}
if (ch == L'\177') ch = L'\b';
bool update = true; static bool meta;
if (esc) { if (ch == L'\33') {
meta = true;
return false;
}
if (meta) {
bool update = true;
switch (ch) { switch (ch) {
break; case L'b': edit(ui.tag, EDIT_BACK_WORD, 0); break; case L'b': edit(ui.view->tag, EDIT_BACK_WORD, 0);
break; case L'f': edit(ui.tag, EDIT_FORE_WORD, 0); break; case L'f': edit(ui.view->tag, EDIT_FORE_WORD, 0);
break; case L'\b': edit(ui.tag, EDIT_KILL_BACK_WORD, 0); break; case L'\b': edit(ui.view->tag, EDIT_KILL_BACK_WORD, 0);
break; case L'd': edit(ui.tag, EDIT_KILL_FORE_WORD, 0); break; case L'd': edit(ui.view->tag, EDIT_KILL_FORE_WORD, 0);
break; default: { break; default: {
update = false; update = false;
if (ch >= L'0' && ch <= L'9') { if (ch < L'0' || ch > L'9') break;
struct Tag tag = tagNum(ch - L'0'); uiViewNum(ch - L'0');
if (tag.name) uiFocus(tag);
}
} }
} }
esc = false; meta = false;
return update; return update;
} }
if (ch == L'\177') ch = L'\b';
switch (ch) { switch (ch) {
break; case CTRL(L'L'): uiRedraw(); return false; break; case CTRL(L'L'): uiRedraw(); return false;
break; case CTRL(L'A'): edit(ui.tag, EDIT_HOME, 0); break; case CTRL(L'A'): edit(ui.view->tag, EDIT_HOME, 0);
break; case CTRL(L'B'): edit(ui.tag, EDIT_LEFT, 0); break; case CTRL(L'B'): edit(ui.view->tag, EDIT_LEFT, 0);
break; case CTRL(L'D'): edit(ui.tag, EDIT_DELETE, 0); break; case CTRL(L'D'): edit(ui.view->tag, EDIT_DELETE, 0);
break; case CTRL(L'E'): edit(ui.tag, EDIT_END, 0); break; case CTRL(L'E'): edit(ui.view->tag, EDIT_END, 0);
break; case CTRL(L'F'): edit(ui.tag, EDIT_RIGHT, 0); break; case CTRL(L'F'): edit(ui.view->tag, EDIT_RIGHT, 0);
break; case CTRL(L'K'): edit(ui.tag, EDIT_KILL_LINE, 0); break; case CTRL(L'K'): edit(ui.view->tag, EDIT_KILL_LINE, 0);
break; case CTRL(L'W'): edit(ui.tag, EDIT_KILL_BACK_WORD, 0); break; case CTRL(L'W'): edit(ui.view->tag, EDIT_KILL_BACK_WORD, 0);
break; case CTRL(L'C'): edit(ui.tag, EDIT_INSERT, IRC_COLOR); break; case CTRL(L'C'): edit(ui.view->tag, EDIT_INSERT, IRC_COLOR);
break; case CTRL(L'N'): edit(ui.tag, EDIT_INSERT, IRC_RESET); break; case CTRL(L'N'): edit(ui.view->tag, EDIT_INSERT, IRC_RESET);
break; case CTRL(L'O'): edit(ui.tag, EDIT_INSERT, IRC_BOLD); break; case CTRL(L'O'): edit(ui.view->tag, EDIT_INSERT, IRC_BOLD);
break; case CTRL(L'R'): edit(ui.tag, EDIT_INSERT, IRC_COLOR); break; case CTRL(L'R'): edit(ui.view->tag, EDIT_INSERT, IRC_COLOR);
break; case CTRL(L'T'): edit(ui.tag, EDIT_INSERT, IRC_ITALIC); break; case CTRL(L'T'): edit(ui.view->tag, EDIT_INSERT, IRC_ITALIC);
break; case CTRL(L'U'): edit(ui.tag, EDIT_INSERT, IRC_UNDERLINE); break; case CTRL(L'U'): edit(ui.view->tag, EDIT_INSERT, IRC_UNDERLINE);
break; case CTRL(L'V'): edit(ui.tag, EDIT_INSERT, IRC_REVERSE); break; case CTRL(L'V'): edit(ui.view->tag, EDIT_INSERT, IRC_REVERSE);
break; case L'\b': edit(ui.tag, EDIT_BACKSPACE, 0); break; case L'\b': edit(ui.view->tag, EDIT_BACKSPACE, 0);
break; case L'\t': edit(ui.tag, EDIT_COMPLETE, 0); break; case L'\t': edit(ui.view->tag, EDIT_COMPLETE, 0);
break; case L'\n': edit(ui.tag, EDIT_ENTER, 0); break; case L'\n': edit(ui.view->tag, EDIT_ENTER, 0);
break; default: { break; default: {
if (!iswprint(ch)) return false; if (!iswprint(ch)) return false;
edit(ui.tag, EDIT_INSERT, ch); edit(ui.view->tag, EDIT_INSERT, ch);
} }
} }
return true; return true;
@ -425,15 +439,15 @@ static bool keyChar(wchar_t ch) {
static bool keyCode(wchar_t ch) { static bool keyCode(wchar_t ch) {
switch (ch) { switch (ch) {
break; case KEY_RESIZE: uiResize(); return false; break; case KEY_RESIZE: uiResize(); return false;
break; case KEY_PPAGE: logUp(); return false; break; case KEY_PPAGE: logPageUp(); return false;
break; case KEY_NPAGE: logDown(); return false; break; case KEY_NPAGE: logPageDown(); return false;
break; case KEY_LEFT: edit(ui.tag, EDIT_LEFT, ch); break; case KEY_LEFT: edit(ui.view->tag, EDIT_LEFT, 0);
break; case KEY_RIGHT: edit(ui.tag, EDIT_RIGHT, ch); break; case KEY_RIGHT: edit(ui.view->tag, EDIT_RIGHT, 0);
break; case KEY_HOME: edit(ui.tag, EDIT_HOME, ch); break; case KEY_HOME: edit(ui.view->tag, EDIT_HOME, 0);
break; case KEY_END: edit(ui.tag, EDIT_END, ch); break; case KEY_END: edit(ui.view->tag, EDIT_END, 0);
break; case KEY_DC: edit(ui.tag, EDIT_DELETE, ch); break; case KEY_DC: edit(ui.view->tag, EDIT_DELETE, 0);
break; case KEY_BACKSPACE: edit(ui.tag, EDIT_BACKSPACE, ch); break; case KEY_BACKSPACE: edit(ui.view->tag, EDIT_BACKSPACE, 0);
break; case KEY_ENTER: edit(ui.tag, EDIT_ENTER, ch); break; case KEY_ENTER: edit(ui.view->tag, EDIT_ENTER, 0);
} }
return true; return true;
} }
@ -453,14 +467,11 @@ void uiRead(void) {
} }
if (!update) return; if (!update) return;
int y, x;
wmove(ui.input, 1, 0); wmove(ui.input, 1, 0);
addIRC(ui.input, editHead()); addIRC(ui.input, editHead());
int y, x;
getyx(ui.input, y, x); getyx(ui.input, y, x);
addIRC(ui.input, editTail()); addIRC(ui.input, editTail());
wclrtoeol(ui.input); wclrtoeol(ui.input);
wmove(ui.input, y, x); wmove(ui.input, y, x);
} }