Become multi-channel

There's a lot of UI missing for it, but it technically works.
master
Curtis McEnroe 2018-08-10 23:31:20 -04:00
parent e9793b4bce
commit 07c750d25c
No known key found for this signature in database
GPG Key ID: CEA2F97ADCFCD77C
12 changed files with 594 additions and 347 deletions

View File

@ -3,7 +3,7 @@ CFLAGS += -Wall -Wextra -Wpedantic
CFLAGS += -I/usr/local/include -I/usr/local/opt/libressl/include CFLAGS += -I/usr/local/include -I/usr/local/opt/libressl/include
LDFLAGS += -L/usr/local/lib -L/usr/local/opt/libressl/lib LDFLAGS += -L/usr/local/lib -L/usr/local/opt/libressl/lib
LDLIBS = -lcursesw -ltls LDLIBS = -lcursesw -ltls
OBJS = chat.o edit.o handle.o input.o irc.o pls.o tab.o ui.o url.o OBJS = chat.o edit.o handle.o input.o irc.o pls.o tab.o tag.o ui.o url.o
all: tags chat all: tags chat

9
README
View File

@ -3,12 +3,13 @@ Simple IRC client for use over anonymous SSH.
This software requires LibreSSL and targets FreeBSD and Darwin. This software requires LibreSSL and targets FreeBSD and Darwin.
chat.h Shared state and function prototypes chat.h Shared state and function prototypes
chat.c Command line parsing and poll loop chat.c Command line parsing and event loop
tag.c Tag (channel, query) management
handle.c Incoming command handling
input.c Input command handling
irc.c TLS client connection
ui.c Curses UI and mIRC formatting ui.c Curses UI and mIRC formatting
edit.c Line editing edit.c Line editing
irc.c TLS client connection
input.c Input command handling
handle.c Incoming command handling
tab.c Tab-complete tab.c Tab-complete
url.c URL detection url.c URL detection
pls.c Functions which should not have to be written pls.c Functions which should not have to be written

43
chat.c
View File

@ -29,6 +29,22 @@
#include "chat.h" #include "chat.h"
void selfNick(const char *nick) {
free(self.nick);
self.nick = strdup(nick);
if (!self.nick) err(EX_OSERR, "strdup");
}
void selfUser(const char *user) {
free(self.user);
self.user = strdup(user);
if (!self.user) err(EX_OSERR, "strdup");
}
void selfJoin(const char *join) {
free(self.join);
self.join = strdup(join);
if (!self.join) err(EX_OSERR, "strdup");
}
static union { static union {
struct { struct {
struct pollfd ui; struct pollfd ui;
@ -44,7 +60,7 @@ static union {
void spawn(char *const argv[]) { void spawn(char *const argv[]) {
if (fds.pipe.events) { if (fds.pipe.events) {
uiLog(L"spawn: existing pipe"); uiLog(TAG_DEFAULT, L"spawn: existing pipe");
return; return;
} }
@ -77,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("%.*s", (int)len, buf); uiFmt(TAG_DEFAULT, "%.*s", (int)len, buf);
} else { } else {
close(fds.pipe.fd); close(fds.pipe.fd);
fds.pipe.events = 0; fds.pipe.events = 0;
@ -108,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("spawn: exit %d", WEXITSTATUS(status)); uiFmt(TAG_DEFAULT, "spawn: exit %d", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) { } else if (WIFSIGNALED(status)) {
uiFmt("spawn: signal %d", WTERMSIG(status)); uiFmt(TAG_DEFAULT, "spawn: signal %d", WTERMSIG(status));
} }
} }
static void sigint(int sig) { static void sigint(int sig) {
(void)sig; (void)sig;
input("/quit"); input(TAG_DEFAULT, "/quit");
uiExit(); uiExit();
exit(EX_OK); exit(EX_OK);
} }
@ -129,7 +145,7 @@ static char *prompt(const char *prompt) {
fflush(stdout); fflush(stdout);
ssize_t len = getline(&line, &cap, stdin); ssize_t len = getline(&line, &cap, stdin);
if (ferror(stdin)) err(EX_IOERR, "getline"); //if (ferror(stdin)) err(EX_IOERR, "getline");
if (feof(stdin)) exit(EX_OK); if (feof(stdin)) exit(EX_OK);
if (len < 2) continue; if (len < 2) continue;
@ -149,25 +165,24 @@ int main(int argc, char *argv[]) {
switch (opt) { switch (opt) {
break; case 'W': webirc = optarg; break; case 'W': webirc = optarg;
break; case 'h': host = strdup(optarg); break; case 'h': host = strdup(optarg);
break; case 'j': chat.join = strdup(optarg); break; case 'j': selfJoin(optarg);
break; case 'n': chat.nick = strdup(optarg); break; case 'n': selfNick(optarg);
break; case 'p': port = optarg; break; case 'p': port = optarg;
break; case 'u': chat.user = strdup(optarg); break; case 'u': selfUser(optarg);
break; case 'v': chat.verbose = true; break; case 'v': self.verbose = true;
break; case 'w': pass = optarg; break; case 'w': pass = optarg;
break; default: return EX_USAGE; break; default: return EX_USAGE;
} }
} }
if (!host) host = prompt("Host: "); if (!host) host = prompt("Host: ");
if (!chat.join) chat.join = prompt("Join: "); if (!self.nick) self.nick = prompt("Name: ");
if (!chat.nick) chat.nick = prompt("Name: "); if (!self.user) selfUser(self.nick);
if (!chat.user) chat.user = strdup(chat.nick);
inputTab(); inputTab();
uiInit(); uiInit();
uiLog(L"Traveling..."); uiLog(TAG_DEFAULT, L"Traveling...");
uiDraw(); uiDraw();
fds.irc.fd = ircConnect(host, port, pass, webirc); fds.irc.fd = ircConnect(host, port, pass, webirc);

102
chat.h
View File

@ -30,18 +30,23 @@ struct {
char *nick; char *nick;
char *user; char *user;
char *join; char *join;
} chat; } self;
void spawn(char *const argv[]); void selfNick(const char *nick);
void selfUser(const char *user);
void selfJoin(const char *join);
int ircConnect( struct Tag {
const char *host, const char *port, const char *pass, const char *webPass size_t id;
); const char *name;
void ircRead(void); };
void ircWrite(const char *ptr, size_t len);
__attribute__((format(printf, 1, 2))) enum { TAGS_LEN = 256 };
void ircFmt(const char *format, ...); const struct Tag TAG_ALL;
const struct Tag TAG_DEFAULT;
struct Tag tagFor(const char *name);
struct Tag tagName(const char *name);
struct Tag tagNum(size_t num);
enum { enum {
IRC_BOLD = 002, IRC_BOLD = 002,
@ -52,47 +57,72 @@ enum {
IRC_UNDERLINE = 037, IRC_UNDERLINE = 037,
}; };
void handle(char *line);
void input(struct Tag tag, char *line);
void inputTab(void);
int ircConnect(
const char *host, const char *port, const char *pass, const char *webPass
);
void ircRead(void);
void ircWrite(const char *ptr, size_t len);
void ircFmt(const char *format, ...) __attribute__((format(printf, 1, 2)));
void uiInit(void); 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 uiBeep(void);
void uiRead(void); void uiRead(void);
void uiTopic(const wchar_t *topic); void uiFocus(struct Tag tag);
void uiTopicStr(const char *topic); void uiTopic(struct Tag tag, const char *topic);
void uiLog(const wchar_t *line); void uiLog(struct Tag tag, const wchar_t *line);
void uiFmt(const wchar_t *format, ...); void uiFmt(struct Tag tag, const wchar_t *format, ...);
// HACK: clang won't check wchar_t *format strings.
#ifdef NDEBUG
#define uiFmt(format, ...) uiFmt(L##format, __VA_ARGS__)
#else
#define uiFmt(format, ...) do { \
snprintf(NULL, 0, format, __VA_ARGS__); \
uiFmt(L##format, __VA_ARGS__); \
} while(0)
#endif
enum Edit {
EDIT_LEFT,
EDIT_RIGHT,
EDIT_HOME,
EDIT_END,
EDIT_BACK_WORD,
EDIT_FORE_WORD,
EDIT_INSERT,
EDIT_BACKSPACE,
EDIT_DELETE,
EDIT_KILL_BACK_WORD,
EDIT_KILL_FORE_WORD,
EDIT_KILL_LINE,
EDIT_COMPLETE,
EDIT_ENTER,
};
void edit(struct Tag tag, enum Edit op, wchar_t ch);
const wchar_t *editHead(void); const wchar_t *editHead(void);
const wchar_t *editTail(void); const wchar_t *editTail(void);
bool edit(bool meta, bool ctrl, wchar_t ch);
void handle(char *line); void tabTouch(struct Tag tag, const char *word);
void tabRemove(struct Tag tag, const char *word);
void inputTab(void); void tabClear(struct Tag tag);
void input(char *line);
void urlScan(const char *str);
void urlList(void);
void urlOpen(size_t i);
void tabTouch(const char *word);
void tabRemove(const char *word);
void tabReplace(const char *prev, const char *next); void tabReplace(const char *prev, const char *next);
const char *tabNext(const char *prefix); const char *tabNext(struct Tag tag, const char *prefix);
void tabAccept(void); void tabAccept(void);
void tabReject(void); void tabReject(void);
void urlScan(struct Tag tag, const char *str);
void urlList(struct Tag tag);
void urlOpen(struct Tag tag, size_t fromEnd);
void spawn(char *const argv[]);
wchar_t *ambstowcs(const char *src); wchar_t *ambstowcs(const char *src);
char *awcstombs(const wchar_t *src); char *awcstombs(const wchar_t *src);
int vaswprintf(wchar_t **ret, const wchar_t *format, va_list ap); int vaswprintf(wchar_t **ret, const wchar_t *format, va_list ap);
// HACK: clang won't check wchar_t *format strings.
#ifdef NDEBUG
#define uiFmt(tag, format, ...) uiFmt(tag, L##format, __VA_ARGS__)
#else
#define uiFmt(tag, format, ...) do { \
snprintf(NULL, 0, format, __VA_ARGS__); \
uiFmt(tag, L##format, __VA_ARGS__); \
} while(0)
#endif

141
edit.c
View File

@ -34,6 +34,7 @@ static struct {
.end = line.buf, .end = line.buf,
}; };
// XXX: editTail must always be called after editHead.
static wchar_t tail; static wchar_t tail;
const wchar_t *editHead(void) { const wchar_t *editHead(void) {
tail = *line.ptr; tail = *line.ptr;
@ -41,8 +42,9 @@ const wchar_t *editHead(void) {
return line.buf; return line.buf;
} }
const wchar_t *editTail(void) { const wchar_t *editTail(void) {
*line.ptr = tail; if (tail) *line.ptr = tail;
*line.end = L'\0'; *line.end = L'\0';
tail = L'\0';
return line.ptr; return line.ptr;
} }
@ -52,13 +54,29 @@ static void left(void) {
static void right(void) { static void right(void) {
if (line.ptr < line.end) line.ptr++; if (line.ptr < line.end) line.ptr++;
} }
static void home(void) {
line.ptr = line.buf; static void backWord(void) {
left();
editHead();
wchar_t *word = wcsrchr(line.buf, ' ');
editTail();
line.ptr = (word ? &word[1] : line.buf);
} }
static void end(void) { static void foreWord(void) {
line.ptr = line.end; right();
editTail();
wchar_t *word = wcschr(line.ptr, ' ');
line.ptr = (word ? word : line.end);
} }
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) { static void backspace(void) {
if (line.ptr == line.buf) return; if (line.ptr == line.buf) return;
if (line.ptr != line.end) { if (line.ptr != line.end) {
@ -73,41 +91,6 @@ static void delete(void) {
backspace(); 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 void backWord(void) {
left();
editHead();
wchar_t *word = wcsrchr(line.buf, ' ');
editTail();
line.ptr = (word ? &word[1] : line.buf);
}
static void foreWord(void) {
right();
editHead();
editTail();
wchar_t *word = wcschr(line.ptr, ' ');
line.ptr = (word ? word : line.end);
}
static void killBackWord(void) { static void killBackWord(void) {
wchar_t *from = line.ptr; wchar_t *from = line.ptr;
backWord(); backWord();
@ -121,12 +104,9 @@ static void killForeWord(void) {
line.end -= line.ptr - from; line.end -= line.ptr - from;
line.ptr = from; line.ptr = from;
} }
static void killLine(void) {
line.end = line.ptr;
}
static char *prefix; static char *prefix;
static void complete(void) { static void complete(struct Tag tag) {
if (!line.tab) { if (!line.tab) {
editHead(); editHead();
line.tab = wcsrchr(line.buf, L' '); line.tab = wcsrchr(line.buf, L' ');
@ -136,7 +116,7 @@ static void complete(void) {
editTail(); editTail();
} }
const char *next = tabNext(prefix); const char *next = tabNext(tag, prefix);
if (!next) return; if (!next) return;
wchar_t *wcs = ambstowcs(next); wchar_t *wcs = ambstowcs(next);
@ -179,52 +159,37 @@ static void reject(void) {
tabReject(); tabReject();
} }
static bool editMeta(wchar_t ch) { static void enter(struct Tag tag) {
switch (ch) { if (line.end == line.buf) return;
break; case L'b': reject(); backWord(); editTail();
break; case L'f': reject(); foreWord(); char *str = awcstombs(line.buf);
break; case L'\b': reject(); killBackWord(); if (!str) err(EX_DATAERR, "awcstombs");
break; case L'd': reject(); killForeWord(); input(tag, str);
free(str);
break; default: return false; line.ptr = line.buf;
} line.end = line.buf;
return true;
} }
static bool editCtrl(wchar_t ch) { void edit(struct Tag tag, enum Edit op, wchar_t ch) {
switch (ch) { switch (op) {
break; case L'B': reject(); left(); break; case EDIT_LEFT: reject(); left();
break; case L'F': reject(); right(); break; case EDIT_RIGHT: reject(); right();
break; case L'A': reject(); home(); break; case EDIT_HOME: reject(); line.ptr = line.buf;
break; case L'E': reject(); end(); break; case EDIT_END: reject(); line.ptr = line.end;
break; case L'D': reject(); delete();
break; case L'W': reject(); killBackWord();
break; case L'K': reject(); killLine();
break; case L'C': accept(); insert(IRC_COLOR); break; case EDIT_BACK_WORD: reject(); backWord();
break; case L'N': accept(); insert(IRC_RESET); break; case EDIT_FORE_WORD: reject(); foreWord();
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; break; case EDIT_INSERT: accept(); insert(ch);
break; case EDIT_BACKSPACE: reject(); backspace();
break; case EDIT_DELETE: reject(); delete();
break; case EDIT_KILL_BACK_WORD: reject(); killBackWord();
break; case EDIT_KILL_FORE_WORD: reject(); killForeWord();
break; case EDIT_KILL_LINE: reject(); line.end = line.ptr;
break; case EDIT_COMPLETE: complete(tag);
break; case EDIT_ENTER: accept(); enter(tag);
} }
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;
} }

163
handle.c
View File

@ -74,6 +74,16 @@ static void shift(
va_end(ap); va_end(ap);
} }
static bool isSelf(const char *nick, const char *user) {
if (!user) return false;
if (!strcmp(user, self.user)) return true;
if (!strcmp(nick, self.nick)) {
if (strcmp(user, self.user)) selfUser(user);
return true;
}
return false;
}
typedef void (*Handler)(char *prefix, char *params); typedef void (*Handler)(char *prefix, char *params);
static void handlePing(char *prefix, char *params) { static void handlePing(char *prefix, char *params) {
@ -84,104 +94,118 @@ static void handlePing(char *prefix, char *params) {
static void handle432(char *prefix, char *params) { static void handle432(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(L"You can't use that name here"); uiLog(TAG_DEFAULT, L"You can't use that name here");
uiFmt("Sheriff says, \"%s\"", mesg); uiFmt(TAG_DEFAULT, "Sheriff says, \"%s\"", mesg);
uiLog(L"Type /nick <name> to choose a new one"); uiLog(TAG_DEFAULT, L"Type /nick <name> to choose a new one");
} }
static void handle001(char *prefix, char *params) { static void handle001(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, chat.nick)) { if (strcmp(nick, self.nick)) selfNick(nick);
free(chat.nick); tabTouch(TAG_DEFAULT, self.nick);
chat.nick = strdup(nick); if (self.join) ircFmt("JOIN %s\r\n", self.join);
} uiLog(TAG_DEFAULT, L"You have arrived");
ircFmt("JOIN %s\r\n", chat.join); }
static void handle372(char *prefix, char *params) {
char *mesg;
shift(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &mesg);
if (mesg[0] == '-' && mesg[1] == ' ') mesg = &mesg[2];
uiFmt(TAG_DEFAULT, "%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);
if (isSelf(nick, user)) {
tabTouch(TAG_DEFAULT, chan);
uiFocus(tag);
} else {
tabTouch(tag, nick);
}
uiFmt( uiFmt(
"\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 (!strcmp(nick, chat.nick) && strcmp(user, chat.user)) {
free(chat.user);
chat.user = strdup(user);
}
tabTouch(nick);
} }
static void handlePart(char *prefix, char *params) { static void handlePart(char *prefix, char *params) {
char *nick, *user, *chan, *mesg; char *nick, *user, *chan, *mesg;
shift(prefix, &nick, &user, NULL, params, 1, 1, &chan, &mesg); shift(prefix, &nick, &user, NULL, params, 1, 1, &chan, &mesg);
struct Tag tag = tagFor(chan);
(void)(isSelf(nick, user) ? tabClear(tag) : tabRemove(tag, nick));
if (mesg) { if (mesg) {
uiFmt( uiFmt(
"\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
); );
} else { } else {
uiFmt( uiFmt(
"\3%d%s\3 leaves \3%d%s\3", tag, "\3%d%s\3 leaves \3%d%s\3",
color(user), nick, color(chan), chan color(user), nick, color(chan), chan
); );
} }
tabRemove(nick);
}
static void handleQuit(char *prefix, char *params) {
char *nick, *user, *mesg;
shift(prefix, &nick, &user, NULL, params, 0, 1, &mesg);
if (mesg) {
char *quot = (mesg[0] == '"') ? "" : "\"";
uiFmt(
"\3%d%s\3 leaves, %s%s%s",
color(user), nick, quot, mesg, quot
);
} else {
uiFmt("\3%d%s\3 leaves", color(user), nick);
}
tabRemove(nick);
} }
static void handleKick(char *prefix, char *params) { static void handleKick(char *prefix, char *params) {
char *nick, *user, *chan, *kick, *mesg; char *nick, *user, *chan, *kick, *mesg;
shift(prefix, &nick, &user, NULL, params, 2, 1, &chan, &kick, &mesg); shift(prefix, &nick, &user, NULL, params, 2, 1, &chan, &kick, &mesg);
struct Tag tag = tagFor(chan);
(void)(isSelf(nick, user) ? tabClear(tag) : tabRemove(tag, nick));
if (mesg) { if (mesg) {
uiFmt( uiFmt(
"\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
); );
} else { } else {
uiFmt( uiFmt(
"\3%d%s\3 kicks \3%d%s\3 out of \3%d%s\3", tag, "\3%d%s\3 kicks \3%d%s\3 out of \3%d%s\3",
color(user), nick, color(kick), kick, color(chan), chan color(user), nick, color(kick), kick, color(chan), chan
); );
} }
tabRemove(nick); }
static void handleQuit(char *prefix, char *params) {
char *nick, *user, *mesg;
shift(prefix, &nick, &user, NULL, params, 0, 1, &mesg);
// TODO: Send to tags where nick is in tab.
tabRemove(TAG_ALL, nick);
if (mesg) {
char *quot = (mesg[0] == '"') ? "" : "\"";
uiFmt(
TAG_DEFAULT, "\3%d%s\3 leaves, %s%s%s",
color(user), nick, quot, mesg, quot
);
} else {
uiFmt(TAG_DEFAULT, "\3%d%s\3 leaves", color(user), nick);
}
} }
static void handle332(char *prefix, char *params) { static void handle332(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);
urlScan(tag, topic);
uiTopic(tag, topic);
uiFmt( uiFmt(
"The sign in \3%d%s\3 reads, \"%s\"", tag, "The sign in \3%d%s\3 reads, \"%s\"",
color(chan), chan, topic color(chan), chan, topic
); );
urlScan(topic);
uiTopicStr(topic);
} }
static void handleTopic(char *prefix, char *params) { static void handleTopic(char *prefix, char *params) {
char *nick, *user, *chan, *topic; char *nick, *user, *chan, *topic;
shift(prefix, &nick, &user, NULL, params, 2, 0, &chan, &topic); shift(prefix, &nick, &user, NULL, params, 2, 0, &chan, &topic);
struct Tag tag = tagFor(chan);
if (!isSelf(nick, user)) tabTouch(tag, nick);
urlScan(tag, topic);
uiTopic(tag, topic);
uiFmt( uiFmt(
"\3%d%s\3 places a new sign in \3%d%s\3, \"%s\"", tag, "\3%d%s\3 places a new sign in \3%d%s\3, \"%s\"",
color(user), nick, color(chan), chan, topic color(user), nick, color(chan), chan, topic
); );
urlScan(topic);
uiTopicStr(topic);
} }
static void handle366(char *prefix, char *params) { static void handle366(char *prefix, char *params) {
@ -190,17 +214,20 @@ static void handle366(char *prefix, char *params) {
ircFmt("WHO %s\r\n", chan); ircFmt("WHO %s\r\n", chan);
} }
// FIXME: Track tag?
static struct { static struct {
char buf[4096]; char buf[4096];
size_t len; size_t len;
} who; } who;
static void handle352(char *prefix, char *params) { static void handle352(char *prefix, char *params) {
char *user, *nick; char *chan, *user, *nick;
shift( shift(
prefix, NULL, NULL, NULL, prefix, NULL, NULL, NULL,
params, 6, 0, NULL, NULL, &user, NULL, NULL, &nick params, 6, 0, NULL, &chan, &user, NULL, NULL, &nick
); );
struct Tag tag = tagFor(chan);
if (!isSelf(nick, user)) 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,
@ -208,14 +235,14 @@ static void handle352(char *prefix, char *params) {
(who.len ? ", " : ""), color(user), nick (who.len ? ", " : ""), color(user), nick
); );
if ((size_t)len < cap) who.len += len; if ((size_t)len < cap) who.len += len;
tabTouch(nick);
} }
static void handle315(char *prefix, char *params) { static void handle315(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);
uiFmt( uiFmt(
"In \3%d%s\3 are %s", tag, "In \3%d%s\3 are %s",
color(chan), chan, who.buf color(chan), chan, who.buf
); );
who.len = 0; who.len = 0;
@ -224,58 +251,58 @@ static void handle315(char *prefix, char *params) {
static void handleNick(char *prefix, char *params) { 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);
// TODO: Send to tags where prev is in tab.
tabReplace(prev, next);
uiFmt( uiFmt(
"\3%d%s\3 is now known as \3%d%s\3", TAG_DEFAULT, "\3%d%s\3 is now known as \3%d%s\3",
color(user), prev, color(user), next color(user), prev, color(user), next
); );
if (!strcmp(user, chat.user)) {
free(chat.nick);
chat.nick = strdup(next);
}
tabReplace(prev, next);
} }
static void handleCTCP(char *nick, char *user, char *mesg) { static void handleCTCP(struct Tag tag, char *nick, char *user, char *mesg) {
mesg = &mesg[1]; mesg = &mesg[1];
char *ctcp = strsep(&mesg, " "); char *ctcp = strsep(&mesg, " ");
char *params = strsep(&mesg, "\1"); char *params = strsep(&mesg, "\1");
if (strcmp(ctcp, "ACTION")) return; if (strcmp(ctcp, "ACTION")) return;
if (!isSelf(nick, user)) tabTouch(tag, nick);
urlScan(tag, params);
uiFmt( uiFmt(
"\3%d* %s\3 %s", tag, "\3%d* %s\3 %s",
color(user), nick, params color(user), nick, params
); );
if (strcmp(user, chat.user)) tabTouch(nick);
urlScan(params);
} }
static void handlePrivmsg(char *prefix, char *params) { static void handlePrivmsg(char *prefix, char *params) {
char *nick, *user, *mesg; char *nick, *user, *chan, *mesg;
shift(prefix, &nick, &user, NULL, params, 2, 0, NULL, &mesg); shift(prefix, &nick, &user, NULL, params, 2, 0, &chan, &mesg);
struct Tag tag = (strcmp(chan, self.nick) ? tagFor(chan) : tagFor(nick));
if (mesg[0] == '\1') { if (mesg[0] == '\1') {
handleCTCP(nick, user, mesg); handleCTCP(tag, nick, user, mesg);
return; return;
} }
bool self = !strcmp(user, chat.user); if (!isSelf(nick, user)) tabTouch(tag, nick);
bool ping = !strncasecmp(mesg, chat.nick, strlen(chat.nick)); urlScan(tag, mesg);
bool ping = !strncasecmp(mesg, self.nick, strlen(self.nick));
bool self = isSelf(nick, user);
uiFmt( uiFmt(
"%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 (!self) tabTouch(nick);
if (ping) uiBeep(); if (ping) uiBeep();
urlScan(mesg);
} }
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);
if (strcmp(chan, chat.join)) return; struct Tag tag = TAG_DEFAULT;
if (user) tag = (strcmp(chan, self.nick) ? tagFor(chan) : tagFor(nick));
if (!isSelf(nick, user)) tabTouch(tag, nick);
urlScan(tag, mesg);
uiFmt( uiFmt(
"\3%d-%s-\3 %s", tag, "\3%d-%s-\3 %s",
color(user), nick, mesg color(user), nick, mesg
); );
tabTouch(nick);
urlScan(mesg);
} }
static const struct { static const struct {
@ -287,6 +314,8 @@ static const struct {
{ "332", handle332 }, { "332", handle332 },
{ "352", handle352 }, { "352", handle352 },
{ "366", handle366 }, { "366", handle366 },
{ "372", handle372 },
{ "375", handle372 },
{ "432", handle432 }, { "432", handle432 },
{ "433", handle432 }, { "433", handle432 },
{ "JOIN", handleJoin }, { "JOIN", handleJoin },

79
input.c
View File

@ -23,12 +23,13 @@
#include "chat.h" #include "chat.h"
static void privmsg(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(
&line, ":%s!%s %nPRIVMSG %s :%s%s%s", &line, ":%s!%s %nPRIVMSG %s :%s%s%s",
chat.nick, chat.user, &send, chat.join, self.nick, self.user, &send, tag.name,
(action ? "\1ACTION " : ""), mesg, (action ? "\1" : "") (action ? "\1ACTION " : ""), mesg, (action ? "\1" : "")
); );
if (!line) err(EX_OSERR, "asprintf"); if (!line) err(EX_OSERR, "asprintf");
@ -37,35 +38,47 @@ static void privmsg(bool action, const char *mesg) {
free(line); free(line);
} }
typedef void (*Handler)(char *params); typedef void (*Handler)(struct Tag tag, char *params);
static void inputMe(char *params) { static void inputMe(struct Tag tag, char *params) {
privmsg(true, params ? params : ""); privmsg(tag, true, params ? params : "");
} }
static void inputNick(char *params) { static void inputNick(struct Tag tag, char *params) {
(void)tag;
char *nick = strsep(&params, " "); char *nick = strsep(&params, " ");
if (nick) { if (nick) {
ircFmt("NICK %s\r\n", nick); ircFmt("NICK %s\r\n", nick);
} else { } else {
uiLog(L"/nick requires a name"); uiLog(TAG_DEFAULT, L"/nick requires a name");
} }
} }
static void inputWho(char *params) { static void inputJoin(struct Tag tag, char *params) {
(void)params; (void)tag;
ircFmt("WHO %s\r\n", chat.join); char *chan = strsep(&params, " ");
} if (chan) {
ircFmt("JOIN %s\r\n", chan);
static void inputTopic(char *params) {
if (params) {
ircFmt("TOPIC %s :%s\r\n", chat.join, params);
} else { } else {
ircFmt("TOPIC %s\r\n", chat.join); uiLog(TAG_DEFAULT, L"/join requires a channel");
} }
} }
static void inputQuit(char *params) { static void inputWho(struct Tag tag, char *params) {
(void)params; // TODO
ircFmt("WHO %s\r\n", tag.name);
}
static void inputTopic(struct Tag tag, char *params) {
if (params) { // TODO
ircFmt("TOPIC %s :%s\r\n", tag.name, params);
} else {
ircFmt("TOPIC %s\r\n", tag.name);
}
}
static void inputQuit(struct Tag tag, char *params) {
(void)tag;
if (params) { if (params) {
ircFmt("QUIT :%s\r\n", params); ircFmt("QUIT :%s\r\n", params);
} else { } else {
@ -73,25 +86,34 @@ static void inputQuit(char *params) {
} }
} }
static void inputUrl(char *params) { static void inputUrl(struct Tag tag, char *params) {
(void)params; (void)params;
urlList(); urlList(tag);
} }
static void inputOpen(char *params) { static void inputOpen(struct Tag tag, char *params) {
if (!params) { urlOpen(1); return; } if (!params) { urlOpen(tag, 1); return; }
size_t from = strtoul(strsep(&params, "-,"), NULL, 0); size_t from = strtoul(strsep(&params, "-,"), NULL, 0);
if (!params) { urlOpen(from); return; } if (!params) { urlOpen(tag, from); return; }
size_t to = strtoul(strsep(&params, "-,"), NULL, 0); size_t to = strtoul(strsep(&params, "-,"), NULL, 0);
if (to < from) to = from; if (to < from) to = from;
for (size_t i = from; i <= to; ++i) { for (size_t i = from; i <= to; ++i) {
urlOpen(i); urlOpen(tag, i);
} }
} }
static void inputView(struct Tag tag, char *params) {
char *view = strsep(&params, " ");
if (!view) return;
size_t num = strtoul(view, &view, 0);
tag = (view[0] ? tagName(view) : tagNum(num));
if (tag.name) uiFocus(tag);
}
static const struct { static const struct {
const char *command; const char *command;
Handler handler; Handler handler;
} COMMANDS[] = { } COMMANDS[] = {
{ "/join", inputJoin },
{ "/me", inputMe }, { "/me", inputMe },
{ "/names", inputWho }, { "/names", inputWho },
{ "/nick", inputNick }, { "/nick", inputNick },
@ -99,27 +121,28 @@ static const struct {
{ "/quit", inputQuit }, { "/quit", inputQuit },
{ "/topic", inputTopic }, { "/topic", inputTopic },
{ "/url", inputUrl }, { "/url", inputUrl },
{ "/view", inputView },
{ "/who", inputWho }, { "/who", inputWho },
}; };
static const size_t COMMANDS_LEN = sizeof(COMMANDS) / sizeof(COMMANDS[0]); static const size_t COMMANDS_LEN = sizeof(COMMANDS) / sizeof(COMMANDS[0]);
void input(char *input) { void input(struct Tag tag, char *input) {
if (input[0] != '/') { if (input[0] != '/') {
privmsg(false, input); privmsg(tag, false, input);
return; return;
} }
char *command = strsep(&input, " "); char *command = strsep(&input, " ");
if (input && !input[0]) input = NULL; if (input && !input[0]) input = NULL;
for (size_t i = 0; i < COMMANDS_LEN; ++i) { for (size_t i = 0; i < COMMANDS_LEN; ++i) {
if (strcasecmp(command, COMMANDS[i].command)) continue; if (strcasecmp(command, COMMANDS[i].command)) continue;
COMMANDS[i].handler(input); COMMANDS[i].handler(tag, input);
return; return;
} }
uiFmt("%s isn't a recognized command", command); uiFmt(TAG_DEFAULT, "%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(COMMANDS[i].command); tabTouch(TAG_DEFAULT, COMMANDS[i].command);
} }
} }

13
irc.c
View File

@ -39,7 +39,7 @@ static void webirc(const char *pass) {
if (sp) len = sp - ssh; if (sp) len = sp - ssh;
ircFmt( ircFmt(
"WEBIRC %s %s %.*s %.*s\r\n", "WEBIRC %s %s %.*s %.*s\r\n",
pass, chat.user, len, ssh, len, ssh pass, self.user, len, ssh, len, ssh
); );
} }
@ -83,11 +83,8 @@ int ircConnect(
if (webPass) webirc(webPass); if (webPass) webirc(webPass);
if (pass) ircFmt("PASS :%s\r\n", pass); if (pass) ircFmt("PASS :%s\r\n", pass);
ircFmt( ircFmt("NICK %s\r\n", self.nick);
"NICK %s\r\n" ircFmt("USER %s 0 * :%s\r\n", self.user, self.nick);
"USER %s 0 * :%s\r\n",
chat.nick, chat.user, chat.nick
);
return sock; return sock;
} }
@ -109,7 +106,7 @@ 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 (chat.verbose) uiFmt("<<< %.*s", len - 2, buf); if (self.verbose) uiFmt(tagFor("(irc)"), "\00314<<<\3 %.*s", len - 2, buf);
ircWrite(buf, len); ircWrite(buf, len);
free(buf); free(buf);
} }
@ -129,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 (chat.verbose) uiFmt(">>> %s", line); if (self.verbose) uiFmt(tagFor("(irc)"), "\00314>>>\3 %s", line);
handle(line); handle(line);
line = &crlf[2]; line = &crlf[2];
} }

35
tab.c
View File

@ -22,6 +22,7 @@
#include "chat.h" #include "chat.h"
static struct Entry { static struct Entry {
size_t tag;
char *word; char *word;
struct Entry *prev; struct Entry *prev;
struct Entry *next; struct Entry *next;
@ -46,8 +47,9 @@ static void touch(struct Entry *entry) {
prepend(entry); prepend(entry);
} }
void tabTouch(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 (strcmp(entry->word, word)) continue; if (strcmp(entry->word, word)) continue;
touch(entry); touch(entry);
return; return;
@ -55,20 +57,28 @@ void tabTouch(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->word = strdup(word); entry->word = strdup(word);
if (!entry->word) err(EX_OSERR, "strdup");
prepend(entry); prepend(entry);
} }
void tabReplace(const char *prev, const char *next) { void tabReplace(const char *prev, const char *next) {
tabTouch(prev); for (struct Entry *entry = head; entry; entry = entry->next) {
free(head->word); if (strcmp(entry->word, prev)) continue;
head->word = strdup(next); free(entry->word);
entry->word = strdup(next);
if (!entry->word) err(EX_OSERR, "strdup");
}
} }
static struct Entry *match; static struct Entry *match;
void tabRemove(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 (strcmp(entry->word, word)) continue; if (strcmp(entry->word, word)) continue;
unlink(entry); unlink(entry);
if (match == entry) match = entry->next; if (match == entry) match = entry->next;
@ -78,17 +88,28 @@ void tabRemove(const char *word) {
} }
} }
const char *tabNext(const char *prefix) { void tabClear(struct Tag tag) {
for (struct Entry *entry = head; entry; entry = entry->next) {
if (entry->tag != tag.id) continue;
unlink(entry);
if (match == entry) match = entry->next;
free(entry->word);
free(entry);
}
}
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 = (match ? match->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 (strncasecmp(entry->word, prefix, len)) continue; if (strncasecmp(entry->word, prefix, len)) continue;
match = entry; match = entry;
return entry->word; return entry->word;
} }
if (!match) return NULL; if (!match) return NULL;
match = NULL; match = NULL;
return tabNext(prefix); return tabNext(tag, prefix);
} }
void tabAccept(void) { void tabAccept(void) {

77
tag.c 100644
View File

@ -0,0 +1,77 @@
/* 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 <stdlib.h>
#include <string.h>
#include <sysexits.h>
#include "chat.h"
const struct Tag TAG_ALL = { (size_t)-1, NULL };
const struct Tag TAG_DEFAULT = { 0, "(status)" };
static struct {
char *name[TAGS_LEN];
size_t len;
size_t gap;
} tags = {
.name = { "(status)" },
.len = 1,
.gap = 1,
};
static struct Tag Tag(size_t id) {
return (struct Tag) { id, tags.name[id] };
}
struct Tag tagName(const char *name) {
for (size_t id = 0; id < tags.len; ++id) {
if (!tags.name[id] || strcmp(tags.name[id], name)) continue;
return Tag(id);
}
return TAG_ALL;
}
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);
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);
}

227
ui.c
View File

@ -95,13 +95,19 @@ static int logHeight(void) {
return LINES - 4; return LINES - 4;
} }
static struct { struct View {
WINDOW *topic; WINDOW *topic;
WINDOW *log; WINDOW *log;
WINDOW *input;
bool hide;
bool mark;
int scroll; int scroll;
bool mark;
};
static struct {
bool hide;
WINDOW *input;
struct Tag tag;
struct View views[TAGS_LEN];
size_t len;
} ui; } ui;
void uiInit(void) { void uiInit(void) {
@ -113,14 +119,7 @@ void uiInit(void) {
focusEnable(); focusEnable();
colorInit(); colorInit();
ui.topic = newpad(2, TOPIC_COLS); ui.tag = TAG_DEFAULT;
mvwhline(ui.topic, 1, 0, ACS_HLINE, TOPIC_COLS);
ui.log = newpad(LOG_LINES, COLS);
wsetscrreg(ui.log, 0, LOG_LINES - 1);
scrollok(ui.log, true);
wmove(ui.log, LOG_LINES - logHeight() - 1, 0);
ui.scroll = LOG_LINES;
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);
@ -130,11 +129,6 @@ void uiInit(void) {
nodelay(ui.input, true); nodelay(ui.input, true);
} }
static void uiResize(void) {
wresize(ui.log, LOG_LINES, COLS);
wmove(ui.log, LOG_LINES - 1, COLS - 1);
}
void uiHide(void) { void uiHide(void) {
ui.hide = true; ui.hide = true;
endwin(); endwin();
@ -149,17 +143,46 @@ void uiExit(void) {
); );
} }
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) {
for (size_t i = 0; i < ui.len; ++i) {
struct View *view = &ui.views[i];
if (!view->log) continue;
wresize(view->log, LOG_LINES, COLS);
wmove(view->log, LOG_LINES - 1, COLS - 1);
}
}
void uiDraw(void) { void uiDraw(void) {
if (ui.hide) return; if (ui.hide) return;
struct View *view = uiView(ui.tag);
pnoutrefresh( pnoutrefresh(
ui.topic, view->topic,
0, 0, 0, 0,
0, 0, 0, 0,
1, lastCol() 1, lastCol()
); );
pnoutrefresh( pnoutrefresh(
ui.log, view->log,
ui.scroll - logHeight(), 0, view->scroll - logHeight(), 0,
2, 0, 2, 0,
lastLine() - 2, lastCol() lastLine() - 2, lastCol()
); );
@ -178,6 +201,16 @@ static void uiRedraw(void) {
clearok(curscr, true); clearok(curscr, true);
} }
void uiFocus(struct Tag tag) {
struct View *view = uiView(ui.tag);
view->mark = true;
view = uiView(tag);
view->mark = false;
touchwin(view->topic);
touchwin(view->log);
ui.tag = tag;
}
void uiBeep(void) { void uiBeep(void) {
beep(); // always be beeping beep(); // always be beeping
} }
@ -276,93 +309,133 @@ static void addIRC(WINDOW *win, const wchar_t *str) {
} }
} }
void uiTopic(const wchar_t *topic) { void uiTopic(struct Tag tag, const char *topic) {
wmove(ui.topic, 0, 0);
addIRC(ui.topic, topic);
wclrtoeol(ui.topic);
}
void uiTopicStr(const char *topic) {
wchar_t *wcs = ambstowcs(topic); wchar_t *wcs = ambstowcs(topic);
if (!wcs) err(EX_DATAERR, "ambstowcs"); if (!wcs) err(EX_DATAERR, "ambstowcs");
uiTopic(wcs); struct View *view = uiView(tag);
wmove(view->topic, 0, 0);
addIRC(view->topic, wcs);
wclrtoeol(view->topic);
free(wcs); free(wcs);
} }
void uiLog(const wchar_t *line) { void uiLog(struct Tag tag, const wchar_t *line) {
waddch(ui.log, '\n'); struct View *view = uiView(tag);
if (ui.mark) { waddch(view->log, '\n');
waddch(ui.log, '\n'); if (view->mark) {
ui.mark = false; waddch(view->log, '\n');
view->mark = false;
} }
addIRC(ui.log, line); addIRC(view->log, line);
} }
void uiFmt(const wchar_t *format, ...) { void uiFmt(struct Tag tag, const wchar_t *format, ...) {
wchar_t *buf; wchar_t *buf;
va_list ap; va_list ap;
va_start(ap, format); va_start(ap, format);
vaswprintf(&buf, format, ap); vaswprintf(&buf, format, ap);
va_end(ap); va_end(ap);
if (!buf) err(EX_OSERR, "vaswprintf"); if (!buf) err(EX_OSERR, "vaswprintf");
uiLog(buf); uiLog(tag, buf);
free(buf); free(buf);
} }
static void logUp(void) { static void logUp(void) {
if (ui.scroll == logHeight()) return; struct View *view = uiView(ui.tag);
if (ui.scroll == LOG_LINES) ui.mark = true; if (view->scroll == logHeight()) return;
ui.scroll = MAX(ui.scroll - logHeight() / 2, logHeight()); if (view->scroll == LOG_LINES) view->mark = true;
view->scroll = MAX(view->scroll - logHeight() / 2, logHeight());
} }
static void logDown(void) { static void logDown(void) {
if (ui.scroll == LOG_LINES) return; struct View *view = uiView(ui.tag);
ui.scroll = MIN(ui.scroll + logHeight() / 2, LOG_LINES); if (view->scroll == LOG_LINES) return;
if (ui.scroll == LOG_LINES) ui.mark = false; view->scroll = MIN(view->scroll + logHeight() / 2, LOG_LINES);
if (view->scroll == LOG_LINES) view->mark = false;
} }
static bool keyChar(wint_t ch) { static bool keyChar(wchar_t ch) {
static bool esc, csi; static bool esc, csi;
bool update = false; if (ch == L'\33') {
switch (ch) { esc = true;
break; case CTRL('L'): uiRedraw(); return false;
break; case CTRL('['): esc = true; return false; }
break; case L'\b': update = edit(esc, false, L'\b'); if (esc && ch == L'[') {
break; case L'\177': update = edit(esc, false, L'\b'); esc = false;
break; case L'\t': update = edit(esc, false, L'\t'); csi = true;
break; case L'\n': update = edit(esc, false, L'\n'); return false;
break; default: { }
if (esc && ch == L'[') { if (csi) {
csi = true; if (ch == L'O') uiView(ui.tag)->mark = true;
return false; if (ch == L'I') uiView(ui.tag)->mark = false;
} else if (csi) { csi = false;
if (ch == L'O') ui.mark = true; return false;
if (ch == L'I') ui.mark = false; }
} else if (iswcntrl(ch)) { if (ch == L'\177') ch = L'\b';
update = edit(esc, true, UNCTRL(ch));
} else { bool update = true;
update = edit(esc, false, ch); if (esc) {
switch (ch) {
break; case L'b': edit(ui.tag, EDIT_BACK_WORD, 0);
break; case L'f': edit(ui.tag, EDIT_FORE_WORD, 0);
break; case L'\b': edit(ui.tag, EDIT_KILL_BACK_WORD, 0);
break; case L'd': edit(ui.tag, EDIT_KILL_FORE_WORD, 0);
break; default: {
update = false;
if (ch >= L'0' && ch <= L'9') {
struct Tag tag = tagNum(ch - L'0');
if (tag.name) uiFocus(tag);
}
} }
} }
esc = false;
return update;
} }
esc = false;
csi = false; switch (ch) {
return update; break; case CTRL(L'L'): uiRedraw(); return false;
break; case CTRL(L'A'): edit(ui.tag, EDIT_HOME, 0);
break; case CTRL(L'B'): edit(ui.tag, EDIT_LEFT, 0);
break; case CTRL(L'D'): edit(ui.tag, EDIT_DELETE, 0);
break; case CTRL(L'E'): edit(ui.tag, EDIT_END, 0);
break; case CTRL(L'F'): edit(ui.tag, EDIT_RIGHT, 0);
break; case CTRL(L'K'): edit(ui.tag, EDIT_KILL_LINE, 0);
break; case CTRL(L'W'): edit(ui.tag, EDIT_KILL_BACK_WORD, 0);
break; case CTRL(L'C'): edit(ui.tag, EDIT_INSERT, IRC_COLOR);
break; case CTRL(L'N'): edit(ui.tag, EDIT_INSERT, IRC_RESET);
break; case CTRL(L'O'): edit(ui.tag, EDIT_INSERT, IRC_BOLD);
break; case CTRL(L'R'): edit(ui.tag, EDIT_INSERT, IRC_COLOR);
break; case CTRL(L'T'): edit(ui.tag, EDIT_INSERT, IRC_ITALIC);
break; case CTRL(L'U'): edit(ui.tag, EDIT_INSERT, IRC_UNDERLINE);
break; case CTRL(L'V'): edit(ui.tag, EDIT_INSERT, IRC_REVERSE);
break; case L'\b': edit(ui.tag, EDIT_BACKSPACE, 0);
break; case L'\t': edit(ui.tag, EDIT_COMPLETE, 0);
break; case L'\n': edit(ui.tag, EDIT_ENTER, 0);
break; default: {
if (!iswprint(ch)) return false;
edit(ui.tag, EDIT_INSERT, ch);
}
}
return true;
} }
static bool keyCode(wint_t ch) { static bool keyCode(wchar_t ch) {
switch (ch) { switch (ch) {
break; case KEY_RESIZE: uiResize(); break; case KEY_RESIZE: uiResize(); return false;
break; case KEY_PPAGE: logUp(); break; case KEY_PPAGE: logUp(); return false;
break; case KEY_NPAGE: logDown(); break; case KEY_NPAGE: logDown(); return false;
break; case KEY_LEFT: return edit(false, true, 'B'); break; case KEY_LEFT: edit(ui.tag, EDIT_LEFT, ch);
break; case KEY_RIGHT: return edit(false, true, 'F'); break; case KEY_RIGHT: edit(ui.tag, EDIT_RIGHT, ch);
break; case KEY_HOME: return edit(false, true, 'A'); break; case KEY_HOME: edit(ui.tag, EDIT_HOME, ch);
break; case KEY_END: return edit(false, true, 'E'); break; case KEY_END: edit(ui.tag, EDIT_END, ch);
break; case KEY_DC: return edit(false, true, 'D'); break; case KEY_DC: edit(ui.tag, EDIT_DELETE, ch);
break; case KEY_BACKSPACE: return edit(false, false, '\b'); break; case KEY_BACKSPACE: edit(ui.tag, EDIT_BACKSPACE, ch);
break; case KEY_ENTER: return edit(false, false, '\n'); break; case KEY_ENTER: edit(ui.tag, EDIT_ENTER, ch);
} }
return false; return true;
} }
void uiRead(void) { void uiRead(void) {

50
url.c
View File

@ -30,40 +30,56 @@ static const char *SCHEMES[] = {
}; };
static const size_t SCHEMES_LEN = sizeof(SCHEMES) / sizeof(SCHEMES[0]); static const size_t SCHEMES_LEN = sizeof(SCHEMES) / sizeof(SCHEMES[0]);
enum { RING_LEN = 16 }; struct Entry {
static char *ring[RING_LEN]; size_t tag;
static size_t last; char *url;
};
enum { RING_LEN = 32 };
static_assert(!(RING_LEN & (RING_LEN - 1)), "power of two RING_LEN"); static_assert(!(RING_LEN & (RING_LEN - 1)), "power of two RING_LEN");
static void push(const char *url, size_t len) { static struct {
free(ring[last]); struct Entry buf[RING_LEN];
ring[last++] = strndup(url, len); size_t end;
last &= RING_LEN - 1; } ring;
static void push(struct Tag tag, const char *url, size_t len) {
free(ring.buf[ring.end].url);
ring.buf[ring.end].tag = tag.id;
ring.buf[ring.end].url = strndup(url, len);
if (!ring.buf[ring.end].url) err(EX_OSERR, "strndup");
ring.end = (ring.end + 1) & (RING_LEN - 1);
} }
void urlScan(const char *str) { void urlScan(struct Tag tag, const char *str) {
while (str[0]) { while (str[0]) {
size_t len = 1; size_t len = 1;
for (size_t i = 0; i < SCHEMES_LEN; ++i) { for (size_t i = 0; i < SCHEMES_LEN; ++i) {
if (strncmp(str, SCHEMES[i], strlen(SCHEMES[i]))) continue; if (strncmp(str, SCHEMES[i], strlen(SCHEMES[i]))) continue;
len = strcspn(str, " >\""); len = strcspn(str, " >\"");
push(str, len); push(tag, str, len);
} }
str = &str[len]; str = &str[len];
} }
} }
void urlList(void) { void urlList(struct Tag tag) {
uiHide(); uiHide();
for (size_t i = 0; i < RING_LEN; ++i) { for (size_t i = 0; i < RING_LEN; ++i) {
char *url = ring[(i + last) & (RING_LEN - 1)]; struct Entry entry = ring.buf[(ring.end + i) & (RING_LEN - 1)];
if (url) printf("%s\n", url); if (!entry.url || entry.tag != tag.id) continue;
printf("%s\n", entry.url);
} }
} }
void urlOpen(size_t i) { void urlOpen(struct Tag tag, size_t fromEnd) {
char *url = ring[(last - i) & (RING_LEN - 1)]; size_t count = 0;
if (!url) return; for (size_t i = 0; i < RING_LEN; ++i) {
char *argv[] = { "open", url, NULL }; struct Entry entry = ring.buf[(ring.end - i) & (RING_LEN - 1)];
spawn(argv); if (!entry.url || entry.tag != tag.id) continue;
if (++count != fromEnd) continue;
char *argv[] = { "open", entry.url, NULL };
spawn(argv);
return;
}
} }