diff --git a/Makefile b/Makefile index 57369d6..bf7b011 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ CFLAGS += -Wall -Wextra -Wpedantic CFLAGS += -I/usr/local/include -I/usr/local/opt/libressl/include LDFLAGS += -L/usr/local/lib -L/usr/local/opt/libressl/lib 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 diff --git a/README b/README index 8fa4ad7..9a93b9d 100644 --- a/README +++ b/README @@ -3,12 +3,13 @@ Simple IRC client for use over anonymous SSH. This software requires LibreSSL and targets FreeBSD and Darwin. 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 edit.c Line editing - irc.c TLS client connection - input.c Input command handling - handle.c Incoming command handling tab.c Tab-complete url.c URL detection pls.c Functions which should not have to be written diff --git a/chat.c b/chat.c index 332cfd6..c85a1fe 100644 --- a/chat.c +++ b/chat.c @@ -29,6 +29,22 @@ #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 { struct { struct pollfd ui; @@ -44,7 +60,7 @@ static union { void spawn(char *const argv[]) { if (fds.pipe.events) { - uiLog(L"spawn: existing pipe"); + uiLog(TAG_DEFAULT, L"spawn: existing pipe"); return; } @@ -77,7 +93,7 @@ static void pipeRead(void) { if (len) { buf[len] = '\0'; len = strcspn(buf, "\n"); - uiFmt("%.*s", (int)len, buf); + uiFmt(TAG_DEFAULT, "%.*s", (int)len, buf); } else { close(fds.pipe.fd); fds.pipe.events = 0; @@ -108,15 +124,15 @@ static void sigchld(int sig) { pid_t pid = wait(&status); if (pid < 0) err(EX_OSERR, "wait"); if (WIFEXITED(status) && WEXITSTATUS(status)) { - uiFmt("spawn: exit %d", WEXITSTATUS(status)); + uiFmt(TAG_DEFAULT, "spawn: exit %d", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { - uiFmt("spawn: signal %d", WTERMSIG(status)); + uiFmt(TAG_DEFAULT, "spawn: signal %d", WTERMSIG(status)); } } static void sigint(int sig) { (void)sig; - input("/quit"); + input(TAG_DEFAULT, "/quit"); uiExit(); exit(EX_OK); } @@ -129,7 +145,7 @@ static char *prompt(const char *prompt) { fflush(stdout); 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 (len < 2) continue; @@ -149,25 +165,24 @@ int main(int argc, char *argv[]) { switch (opt) { break; case 'W': webirc = optarg; break; case 'h': host = strdup(optarg); - break; case 'j': chat.join = strdup(optarg); - break; case 'n': chat.nick = strdup(optarg); + break; case 'j': selfJoin(optarg); + break; case 'n': selfNick(optarg); break; case 'p': port = optarg; - break; case 'u': chat.user = strdup(optarg); - break; case 'v': chat.verbose = true; + break; case 'u': selfUser(optarg); + break; case 'v': self.verbose = true; break; case 'w': pass = optarg; break; default: return EX_USAGE; } } if (!host) host = prompt("Host: "); - if (!chat.join) chat.join = prompt("Join: "); - if (!chat.nick) chat.nick = prompt("Name: "); - if (!chat.user) chat.user = strdup(chat.nick); + if (!self.nick) self.nick = prompt("Name: "); + if (!self.user) selfUser(self.nick); inputTab(); uiInit(); - uiLog(L"Traveling..."); + uiLog(TAG_DEFAULT, L"Traveling..."); uiDraw(); fds.irc.fd = ircConnect(host, port, pass, webirc); diff --git a/chat.h b/chat.h index 9a1b855..9219334 100644 --- a/chat.h +++ b/chat.h @@ -30,18 +30,23 @@ struct { char *nick; char *user; 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( - const char *host, const char *port, const char *pass, const char *webPass -); -void ircRead(void); -void ircWrite(const char *ptr, size_t len); +struct Tag { + size_t id; + const char *name; +}; -__attribute__((format(printf, 1, 2))) -void ircFmt(const char *format, ...); +enum { TAGS_LEN = 256 }; +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 { IRC_BOLD = 002, @@ -52,47 +57,72 @@ enum { 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 uiHide(void); void uiExit(void); void uiDraw(void); void uiBeep(void); void uiRead(void); -void uiTopic(const wchar_t *topic); -void uiTopicStr(const char *topic); -void uiLog(const wchar_t *line); -void uiFmt(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 +void uiFocus(struct Tag tag); +void uiTopic(struct Tag tag, const char *topic); +void uiLog(struct Tag tag, const wchar_t *line); +void uiFmt(struct Tag tag, const wchar_t *format, ...); +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 *editTail(void); -bool edit(bool meta, bool ctrl, wchar_t ch); -void handle(char *line); - -void inputTab(void); -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 tabTouch(struct Tag tag, const char *word); +void tabRemove(struct Tag tag, const char *word); +void tabClear(struct Tag tag); 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 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); char *awcstombs(const wchar_t *src); 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 diff --git a/edit.c b/edit.c index dde3396..8db92fc 100644 --- a/edit.c +++ b/edit.c @@ -34,6 +34,7 @@ static struct { .end = line.buf, }; +// XXX: editTail must always be called after editHead. static wchar_t tail; const wchar_t *editHead(void) { tail = *line.ptr; @@ -41,8 +42,9 @@ const wchar_t *editHead(void) { return line.buf; } const wchar_t *editTail(void) { - *line.ptr = tail; + if (tail) *line.ptr = tail; *line.end = L'\0'; + tail = L'\0'; return line.ptr; } @@ -52,13 +54,29 @@ static void left(void) { static void right(void) { 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) { - line.ptr = line.end; +static void foreWord(void) { + 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) { if (line.ptr == line.buf) return; if (line.ptr != line.end) { @@ -73,41 +91,6 @@ static void delete(void) { 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) { wchar_t *from = line.ptr; backWord(); @@ -121,12 +104,9 @@ static void killForeWord(void) { line.end -= line.ptr - from; line.ptr = from; } -static void killLine(void) { - line.end = line.ptr; -} static char *prefix; -static void complete(void) { +static void complete(struct Tag tag) { if (!line.tab) { editHead(); line.tab = wcsrchr(line.buf, L' '); @@ -136,7 +116,7 @@ static void complete(void) { editTail(); } - const char *next = tabNext(prefix); + const char *next = tabNext(tag, prefix); if (!next) return; wchar_t *wcs = ambstowcs(next); @@ -179,52 +159,37 @@ static void reject(void) { tabReject(); } -static bool editMeta(wchar_t ch) { - switch (ch) { - break; case L'b': reject(); backWord(); - break; case L'f': reject(); foreWord(); - break; case L'\b': reject(); killBackWord(); - break; case L'd': reject(); killForeWord(); - - break; default: return false; - } - return true; +static void enter(struct Tag tag) { + if (line.end == line.buf) return; + editTail(); + char *str = awcstombs(line.buf); + if (!str) err(EX_DATAERR, "awcstombs"); + input(tag, str); + free(str); + line.ptr = line.buf; + line.end = line.buf; } -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'W': reject(); killBackWord(); - break; case L'K': reject(); killLine(); +void edit(struct Tag tag, enum Edit op, wchar_t ch) { + switch (op) { + break; case EDIT_LEFT: reject(); left(); + break; case EDIT_RIGHT: reject(); right(); + break; case EDIT_HOME: reject(); line.ptr = line.buf; + break; case EDIT_END: reject(); line.ptr = line.end; - 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; case EDIT_BACK_WORD: reject(); backWord(); + break; case EDIT_FORE_WORD: reject(); foreWord(); - 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; } diff --git a/handle.c b/handle.c index be41828..faf44aa 100644 --- a/handle.c +++ b/handle.c @@ -74,6 +74,16 @@ static void shift( 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); 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) { char *mesg; shift(prefix, NULL, NULL, NULL, params, 3, 0, NULL, NULL, &mesg); - uiLog(L"You can't use that name here"); - uiFmt("Sheriff says, \"%s\"", mesg); - uiLog(L"Type /nick to choose a new one"); + uiLog(TAG_DEFAULT, L"You can't use that name here"); + uiFmt(TAG_DEFAULT, "Sheriff says, \"%s\"", mesg); + uiLog(TAG_DEFAULT, L"Type /nick to choose a new one"); } static void handle001(char *prefix, char *params) { char *nick; shift(prefix, NULL, NULL, NULL, params, 1, 0, &nick); - if (strcmp(nick, chat.nick)) { - free(chat.nick); - chat.nick = strdup(nick); - } - ircFmt("JOIN %s\r\n", chat.join); + if (strcmp(nick, self.nick)) selfNick(nick); + tabTouch(TAG_DEFAULT, self.nick); + if (self.join) ircFmt("JOIN %s\r\n", self.join); + uiLog(TAG_DEFAULT, L"You have arrived"); +} + +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) { char *nick, *user, *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( - "\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 ); - 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) { char *nick, *user, *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) { 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 ); } else { 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 ); } - 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) { char *nick, *user, *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) { 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 ); } else { 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 ); } - 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) { char *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( - "The sign in \3%d%s\3 reads, \"%s\"", + tag, "The sign in \3%d%s\3 reads, \"%s\"", color(chan), chan, topic ); - urlScan(topic); - uiTopicStr(topic); } static void handleTopic(char *prefix, char *params) { char *nick, *user, *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( - "\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 ); - urlScan(topic); - uiTopicStr(topic); } static void handle366(char *prefix, char *params) { @@ -190,17 +214,20 @@ static void handle366(char *prefix, char *params) { ircFmt("WHO %s\r\n", chan); } +// FIXME: Track tag? static struct { char buf[4096]; size_t len; } who; static void handle352(char *prefix, char *params) { - char *user, *nick; + char *chan, *user, *nick; shift( 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; int len = snprintf( &who.buf[who.len], cap, @@ -208,14 +235,14 @@ static void handle352(char *prefix, char *params) { (who.len ? ", " : ""), color(user), nick ); if ((size_t)len < cap) who.len += len; - tabTouch(nick); } static void handle315(char *prefix, char *params) { char *chan; shift(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &chan); + struct Tag tag = tagFor(chan); uiFmt( - "In \3%d%s\3 are %s", + tag, "In \3%d%s\3 are %s", color(chan), chan, who.buf ); who.len = 0; @@ -224,58 +251,58 @@ static void handle315(char *prefix, char *params) { static void handleNick(char *prefix, char *params) { char *prev, *user, *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( - "\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 ); - 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]; char *ctcp = strsep(&mesg, " "); char *params = strsep(&mesg, "\1"); if (strcmp(ctcp, "ACTION")) return; + if (!isSelf(nick, user)) tabTouch(tag, nick); + urlScan(tag, params); uiFmt( - "\3%d* %s\3 %s", + tag, "\3%d* %s\3 %s", color(user), nick, params ); - if (strcmp(user, chat.user)) tabTouch(nick); - urlScan(params); } static void handlePrivmsg(char *prefix, char *params) { - char *nick, *user, *mesg; - shift(prefix, &nick, &user, NULL, params, 2, 0, NULL, &mesg); + char *nick, *user, *chan, *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') { - handleCTCP(nick, user, mesg); + handleCTCP(tag, nick, user, mesg); return; } - bool self = !strcmp(user, chat.user); - bool ping = !strncasecmp(mesg, chat.nick, strlen(chat.nick)); + if (!isSelf(nick, user)) tabTouch(tag, nick); + urlScan(tag, mesg); + bool ping = !strncasecmp(mesg, self.nick, strlen(self.nick)); + bool self = isSelf(nick, user); 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 ); - if (!self) tabTouch(nick); if (ping) uiBeep(); - urlScan(mesg); } static void handleNotice(char *prefix, char *params) { char *nick, *user, *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( - "\3%d-%s-\3 %s", + tag, "\3%d-%s-\3 %s", color(user), nick, mesg ); - tabTouch(nick); - urlScan(mesg); } static const struct { @@ -287,6 +314,8 @@ static const struct { { "332", handle332 }, { "352", handle352 }, { "366", handle366 }, + { "372", handle372 }, + { "375", handle372 }, { "432", handle432 }, { "433", handle432 }, { "JOIN", handleJoin }, diff --git a/input.c b/input.c index 56c38bf..f4e3106 100644 --- a/input.c +++ b/input.c @@ -23,12 +23,13 @@ #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; int send; asprintf( &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" : "") ); if (!line) err(EX_OSERR, "asprintf"); @@ -37,35 +38,47 @@ static void privmsg(bool action, const char *mesg) { free(line); } -typedef void (*Handler)(char *params); +typedef void (*Handler)(struct Tag tag, char *params); -static void inputMe(char *params) { - privmsg(true, params ? params : ""); +static void inputMe(struct Tag tag, char *params) { + privmsg(tag, true, params ? params : ""); } -static void inputNick(char *params) { +static void inputNick(struct Tag tag, char *params) { + (void)tag; char *nick = strsep(¶ms, " "); if (nick) { ircFmt("NICK %s\r\n", nick); } else { - uiLog(L"/nick requires a name"); + uiLog(TAG_DEFAULT, L"/nick requires a name"); } } -static void inputWho(char *params) { - (void)params; - ircFmt("WHO %s\r\n", chat.join); -} - -static void inputTopic(char *params) { - if (params) { - ircFmt("TOPIC %s :%s\r\n", chat.join, params); +static void inputJoin(struct Tag tag, char *params) { + (void)tag; + char *chan = strsep(¶ms, " "); + if (chan) { + ircFmt("JOIN %s\r\n", chan); } 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) { ircFmt("QUIT :%s\r\n", params); } 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; - urlList(); + urlList(tag); } -static void inputOpen(char *params) { - if (!params) { urlOpen(1); return; } +static void inputOpen(struct Tag tag, char *params) { + if (!params) { urlOpen(tag, 1); return; } size_t from = strtoul(strsep(¶ms, "-,"), NULL, 0); - if (!params) { urlOpen(from); return; } + if (!params) { urlOpen(tag, from); return; } size_t to = strtoul(strsep(¶ms, "-,"), NULL, 0); if (to < from) to = from; for (size_t i = from; i <= to; ++i) { - urlOpen(i); + urlOpen(tag, i); } } +static void inputView(struct Tag tag, char *params) { + char *view = strsep(¶ms, " "); + 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 { const char *command; Handler handler; } COMMANDS[] = { + { "/join", inputJoin }, { "/me", inputMe }, { "/names", inputWho }, { "/nick", inputNick }, @@ -99,27 +121,28 @@ static const struct { { "/quit", inputQuit }, { "/topic", inputTopic }, { "/url", inputUrl }, + { "/view", inputView }, { "/who", inputWho }, }; 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] != '/') { - privmsg(false, input); + privmsg(tag, false, input); return; } char *command = strsep(&input, " "); if (input && !input[0]) input = NULL; for (size_t i = 0; i < COMMANDS_LEN; ++i) { if (strcasecmp(command, COMMANDS[i].command)) continue; - COMMANDS[i].handler(input); + COMMANDS[i].handler(tag, input); return; } - uiFmt("%s isn't a recognized command", command); + uiFmt(TAG_DEFAULT, "%s isn't a recognized command", command); } void inputTab(void) { for (size_t i = 0; i < COMMANDS_LEN; ++i) { - tabTouch(COMMANDS[i].command); + tabTouch(TAG_DEFAULT, COMMANDS[i].command); } } diff --git a/irc.c b/irc.c index b9bef73..579f23b 100644 --- a/irc.c +++ b/irc.c @@ -39,7 +39,7 @@ static void webirc(const char *pass) { if (sp) len = sp - ssh; ircFmt( "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 (pass) ircFmt("PASS :%s\r\n", pass); - ircFmt( - "NICK %s\r\n" - "USER %s 0 * :%s\r\n", - chat.nick, chat.user, chat.nick - ); + ircFmt("NICK %s\r\n", self.nick); + ircFmt("USER %s 0 * :%s\r\n", self.user, self.nick); return sock; } @@ -109,7 +106,7 @@ void ircFmt(const char *format, ...) { int len = vasprintf(&buf, format, ap); va_end(ap); 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); free(buf); } @@ -129,7 +126,7 @@ void ircRead(void) { char *crlf, *line = buf; while ((crlf = strnstr(line, "\r\n", &buf[len] - line))) { crlf[0] = '\0'; - if (chat.verbose) uiFmt(">>> %s", line); + if (self.verbose) uiFmt(tagFor("(irc)"), "\00314>>>\3 %s", line); handle(line); line = &crlf[2]; } diff --git a/tab.c b/tab.c index a9ddfe5..a6bb795 100644 --- a/tab.c +++ b/tab.c @@ -22,6 +22,7 @@ #include "chat.h" static struct Entry { + size_t tag; char *word; struct Entry *prev; struct Entry *next; @@ -46,8 +47,9 @@ static void touch(struct Entry *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) { + if (entry->tag != tag.id) continue; if (strcmp(entry->word, word)) continue; touch(entry); return; @@ -55,20 +57,28 @@ void tabTouch(const char *word) { struct Entry *entry = malloc(sizeof(*entry)); if (!entry) err(EX_OSERR, "malloc"); + + entry->tag = tag.id; entry->word = strdup(word); + if (!entry->word) err(EX_OSERR, "strdup"); + prepend(entry); } void tabReplace(const char *prev, const char *next) { - tabTouch(prev); - free(head->word); - head->word = strdup(next); + for (struct Entry *entry = head; entry; entry = entry->next) { + if (strcmp(entry->word, prev)) continue; + free(entry->word); + entry->word = strdup(next); + if (!entry->word) err(EX_OSERR, "strdup"); + } } 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) { + if (tag.id != TAG_ALL.id && entry->tag != tag.id) continue; if (strcmp(entry->word, word)) continue; unlink(entry); 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); struct Entry *start = (match ? match->next : head); 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; match = entry; return entry->word; } if (!match) return NULL; match = NULL; - return tabNext(prefix); + return tabNext(tag, prefix); } void tabAccept(void) { diff --git a/tag.c b/tag.c new file mode 100644 index 0000000..014e84c --- /dev/null +++ b/tag.c @@ -0,0 +1,77 @@ +/* Copyright (C) 2018 Curtis McEnroe + * + * 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 . + */ + +#include +#include +#include +#include + +#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); +} diff --git a/ui.c b/ui.c index 9778473..844e777 100644 --- a/ui.c +++ b/ui.c @@ -95,13 +95,19 @@ static int logHeight(void) { return LINES - 4; } -static struct { +struct View { WINDOW *topic; WINDOW *log; - WINDOW *input; - bool hide; - bool mark; int scroll; + bool mark; +}; + +static struct { + bool hide; + WINDOW *input; + struct Tag tag; + struct View views[TAGS_LEN]; + size_t len; } ui; void uiInit(void) { @@ -113,14 +119,7 @@ void uiInit(void) { focusEnable(); colorInit(); - ui.topic = newpad(2, TOPIC_COLS); - 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.tag = TAG_DEFAULT; ui.input = newpad(2, INPUT_COLS); mvwhline(ui.input, 0, 0, ACS_HLINE, INPUT_COLS); @@ -130,11 +129,6 @@ void uiInit(void) { 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) { ui.hide = true; 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) { if (ui.hide) return; + struct View *view = uiView(ui.tag); pnoutrefresh( - ui.topic, + view->topic, 0, 0, 0, 0, 1, lastCol() ); pnoutrefresh( - ui.log, - ui.scroll - logHeight(), 0, + view->log, + view->scroll - logHeight(), 0, 2, 0, lastLine() - 2, lastCol() ); @@ -178,6 +201,16 @@ static void uiRedraw(void) { 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) { beep(); // always be beeping } @@ -276,93 +309,133 @@ static void addIRC(WINDOW *win, const wchar_t *str) { } } -void uiTopic(const wchar_t *topic) { - wmove(ui.topic, 0, 0); - addIRC(ui.topic, topic); - wclrtoeol(ui.topic); -} - -void uiTopicStr(const char *topic) { +void uiTopic(struct Tag tag, const char *topic) { wchar_t *wcs = ambstowcs(topic); 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); } -void uiLog(const wchar_t *line) { - waddch(ui.log, '\n'); - if (ui.mark) { - waddch(ui.log, '\n'); - ui.mark = false; +void uiLog(struct Tag tag, const wchar_t *line) { + struct View *view = uiView(tag); + waddch(view->log, '\n'); + if (view->mark) { + 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; va_list ap; va_start(ap, format); vaswprintf(&buf, format, ap); va_end(ap); if (!buf) err(EX_OSERR, "vaswprintf"); - uiLog(buf); + uiLog(tag, buf); free(buf); } static void logUp(void) { - if (ui.scroll == logHeight()) return; - if (ui.scroll == LOG_LINES) ui.mark = true; - ui.scroll = MAX(ui.scroll - logHeight() / 2, logHeight()); + struct View *view = uiView(ui.tag); + if (view->scroll == logHeight()) return; + if (view->scroll == LOG_LINES) view->mark = true; + view->scroll = MAX(view->scroll - logHeight() / 2, logHeight()); } static void logDown(void) { - if (ui.scroll == LOG_LINES) return; - ui.scroll = MIN(ui.scroll + logHeight() / 2, LOG_LINES); - if (ui.scroll == LOG_LINES) ui.mark = false; + struct View *view = uiView(ui.tag); + if (view->scroll == LOG_LINES) return; + 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; - bool update = false; - switch (ch) { - break; case CTRL('L'): uiRedraw(); - 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 (esc && ch == L'[') { - csi = true; - return false; - } else if (csi) { - if (ch == L'O') ui.mark = true; - if (ch == L'I') ui.mark = false; - } else if (iswcntrl(ch)) { - update = edit(esc, true, UNCTRL(ch)); - } else { - update = edit(esc, false, ch); + if (ch == L'\33') { + esc = true; + 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; + 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; - return update; + + switch (ch) { + 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) { - break; case KEY_RESIZE: uiResize(); - break; case KEY_PPAGE: logUp(); - break; case KEY_NPAGE: logDown(); - 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'); + break; case KEY_RESIZE: uiResize(); return false; + break; case KEY_PPAGE: logUp(); return false; + break; case KEY_NPAGE: logDown(); return false; + break; case KEY_LEFT: edit(ui.tag, EDIT_LEFT, ch); + break; case KEY_RIGHT: edit(ui.tag, EDIT_RIGHT, ch); + break; case KEY_HOME: edit(ui.tag, EDIT_HOME, ch); + break; case KEY_END: edit(ui.tag, EDIT_END, ch); + break; case KEY_DC: edit(ui.tag, EDIT_DELETE, ch); + break; case KEY_BACKSPACE: edit(ui.tag, EDIT_BACKSPACE, ch); + break; case KEY_ENTER: edit(ui.tag, EDIT_ENTER, ch); } - return false; + return true; } void uiRead(void) { diff --git a/url.c b/url.c index 1c57126..b7172ce 100644 --- a/url.c +++ b/url.c @@ -30,40 +30,56 @@ static const char *SCHEMES[] = { }; static const size_t SCHEMES_LEN = sizeof(SCHEMES) / sizeof(SCHEMES[0]); -enum { RING_LEN = 16 }; -static char *ring[RING_LEN]; -static size_t last; +struct Entry { + size_t tag; + char *url; +}; + +enum { RING_LEN = 32 }; static_assert(!(RING_LEN & (RING_LEN - 1)), "power of two RING_LEN"); -static void push(const char *url, size_t len) { - free(ring[last]); - ring[last++] = strndup(url, len); - last &= RING_LEN - 1; +static struct { + struct Entry buf[RING_LEN]; + size_t end; +} 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]) { size_t len = 1; for (size_t i = 0; i < SCHEMES_LEN; ++i) { if (strncmp(str, SCHEMES[i], strlen(SCHEMES[i]))) continue; len = strcspn(str, " >\""); - push(str, len); + push(tag, str, len); } str = &str[len]; } } -void urlList(void) { +void urlList(struct Tag tag) { uiHide(); for (size_t i = 0; i < RING_LEN; ++i) { - char *url = ring[(i + last) & (RING_LEN - 1)]; - if (url) printf("%s\n", url); + struct Entry entry = ring.buf[(ring.end + i) & (RING_LEN - 1)]; + if (!entry.url || entry.tag != tag.id) continue; + printf("%s\n", entry.url); } } -void urlOpen(size_t i) { - char *url = ring[(last - i) & (RING_LEN - 1)]; - if (!url) return; - char *argv[] = { "open", url, NULL }; - spawn(argv); +void urlOpen(struct Tag tag, size_t fromEnd) { + size_t count = 0; + for (size_t i = 0; i < RING_LEN; ++i) { + struct Entry entry = ring.buf[(ring.end - i) & (RING_LEN - 1)]; + if (!entry.url || entry.tag != tag.id) continue; + if (++count != fromEnd) continue; + char *argv[] = { "open", entry.url, NULL }; + spawn(argv); + return; + } }