diff --git a/chat.c b/chat.c index 5542c82..9d5a323 100644 --- a/chat.c +++ b/chat.c @@ -60,7 +60,7 @@ static union { void spawn(char *const argv[]) { if (fds.pipe.events) { - uiLog(TAG_STATUS, L"spawn: existing pipe"); + uiLog(TAG_STATUS, UI_WARM, L"spawn: existing pipe"); return; } @@ -93,7 +93,7 @@ static void pipeRead(void) { if (len) { buf[len] = '\0'; len = strcspn(buf, "\n"); - uiFmt(TAG_STATUS, "spawn: %.*s", (int)len, buf); + uiFmt(TAG_STATUS, UI_WARM, "spawn: %.*s", (int)len, buf); } else { close(fds.pipe.fd); fds.pipe.events = 0; @@ -124,9 +124,9 @@ static void sigchld(int sig) { pid_t pid = wait(&status); if (pid < 0) err(EX_OSERR, "wait"); if (WIFEXITED(status) && WEXITSTATUS(status)) { - uiFmt(TAG_STATUS, "spawn: exit %d", WEXITSTATUS(status)); + uiFmt(TAG_STATUS, UI_WARM, "spawn: exit %d", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { - uiFmt(TAG_STATUS, "spawn: signal %d", WTERMSIG(status)); + uiFmt(TAG_STATUS, UI_WARM, "spawn: signal %d", WTERMSIG(status)); } } @@ -182,7 +182,7 @@ int main(int argc, char *argv[]) { inputTab(); uiInit(); - uiLog(TAG_STATUS, L"Traveling..."); + uiLog(TAG_STATUS, UI_WARM, L"Traveling..."); uiDraw(); fds.irc.fd = ircConnect(host, port, pass, webirc); diff --git a/chat.h b/chat.h index d6dfdf7..c66683b 100644 --- a/chat.h +++ b/chat.h @@ -91,12 +91,19 @@ void uiHide(void); void uiExit(void); void uiDraw(void); void uiRead(void); + void uiViewTag(struct Tag tag); void uiViewNum(int num); void uiCloseTag(struct Tag tag); + +enum UIHeat { + UI_COLD, + UI_WARM, + UI_HOT, +}; 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, ...); +void uiLog(struct Tag tag, enum UIHeat heat, const wchar_t *line); +void uiFmt(struct Tag tag, enum UIHeat heat, const wchar_t *format, ...); enum TermMode { TERM_FOCUS, @@ -155,10 +162,10 @@ 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__) +#define uiFmt(tag, heat, format, ...) uiFmt(tag, heat, L##format, __VA_ARGS__) #else -#define uiFmt(tag, format, ...) do { \ +#define uiFmt(tag, heat, format, ...) do { \ snprintf(NULL, 0, format, __VA_ARGS__); \ - uiFmt(tag, L##format, __VA_ARGS__); \ + uiFmt(tag, heat, L##format, __VA_ARGS__); \ } while(0) #endif diff --git a/handle.c b/handle.c index adc8f59..71fab12 100644 --- a/handle.c +++ b/handle.c @@ -93,8 +93,7 @@ static bool isSelf(const char *nick, const char *user) { return false; } -static bool isPing(const char *nick, const char *user, const char *mesg) { - if (isSelf(nick, user)) return false; +static bool isPing(const char *mesg) { size_t len = strlen(self.nick); const char *match = mesg; while (NULL != (match = strcasestr(match, self.nick))) { @@ -108,6 +107,13 @@ static bool isPing(const char *nick, const char *user, const char *mesg) { return false; } +static char *dequote(char *mesg) { + if (mesg[0] == '"') mesg = &mesg[1]; + size_t len = strlen(mesg); + if (mesg[len - 1] == '"') mesg[len - 1] = '\0'; + return mesg; +} + typedef void (*Handler)(char *prefix, char *params); static void handlePing(char *prefix, char *params) { @@ -118,39 +124,46 @@ static void handlePing(char *prefix, char *params) { static void handleReplyErroneousNickname(char *prefix, char *params) { char *mesg; shift(prefix, NULL, NULL, NULL, params, 3, 0, NULL, NULL, &mesg); - uiLog(TAG_STATUS, L"You can't use that name here"); - uiFmt(TAG_STATUS, "Sheriff says, \"%s\"", mesg); - uiLog(TAG_STATUS, L"Type /nick to choose a new one"); + // FIXME: Better formatting. + uiLog(TAG_STATUS, UI_HOT, L"You can't use that name here"); + uiFmt(TAG_STATUS, UI_HOT, "Sheriff says, \"%s\"", mesg); + uiLog(TAG_STATUS, UI_HOT, L"Type /nick to choose a new one"); } static void handleReplyWelcome(char *prefix, char *params) { char *nick; shift(prefix, NULL, NULL, NULL, params, 1, 0, &nick); + if (strcmp(nick, self.nick)) selfNick(nick); - tabTouch(TAG_STATUS, self.nick); if (self.join) ircFmt("JOIN %s\r\n", self.join); - uiLog(TAG_STATUS, L"You have arrived"); + tabTouch(TAG_STATUS, self.nick); + + uiLog(TAG_STATUS, UI_WARM, L"You have arrived"); } static void handleReplyMOTD(char *prefix, char *params) { char *mesg; shift(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &mesg); if (mesg[0] == '-' && mesg[1] == ' ') mesg = &mesg[2]; + urlScan(TAG_STATUS, mesg); - uiFmt(TAG_STATUS, "%s", mesg); + uiFmt(TAG_STATUS, UI_COLD, "%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_NONE, chan); uiViewTag(tag); } tabTouch(tag, nick); + uiFmt( - tag, "\3%d%s\3 arrives in \3%d%s\3", + tag, UI_COLD, + "\3%d%s\3 arrives in \3%d%s\3", color(user), nick, color(chan), chan ); } @@ -159,16 +172,24 @@ 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 (isSelf(nick, user)) { + tabClear(tag); + } else { + tabRemove(tag, nick); + } + if (mesg) { urlScan(tag, mesg); uiFmt( - tag, "\3%d%s\3 leaves \3%d%s\3, \"%s\"", + tag, UI_COLD, + "\3%d%s\3 leaves \3%d%s\3, \"%s\"", color(user), nick, color(chan), chan, mesg ); } else { uiFmt( - tag, "\3%d%s\3 leaves \3%d%s\3", + tag, UI_COLD, + "\3%d%s\3 leaves \3%d%s\3", color(user), nick, color(chan), chan ); } @@ -178,16 +199,26 @@ 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)); + bool kicked = !strcmp(kick, self.nick); + + if (kicked) { + tabClear(tag); + } else { + tabRemove(tag, kick); + } + if (mesg) { urlScan(tag, mesg); uiFmt( - 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 + tag, (kicked ? UI_HOT : UI_COLD), + "\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, + dequote(mesg) ); } else { uiFmt( - tag, "\3%d%s\3 kicks \3%d%s\3 out of \3%d%s\3", + tag, (kicked ? UI_HOT : UI_COLD), + "\3%d%s\3 kicks \3%d%s\3 out of \3%d%s\3", color(user), nick, color(kick), kick, color(chan), chan ); } @@ -196,18 +227,20 @@ static void handleKick(char *prefix, char *params) { static void handleQuit(char *prefix, char *params) { char *nick, *user, *mesg; shift(prefix, &nick, &user, NULL, params, 0, 1, &mesg); - char *quot = (mesg && mesg[0] == '"') ? "" : "\""; + struct Tag tag; while (TAG_NONE.id != (tag = tabTag(nick)).id) { tabRemove(tag, nick); + if (mesg) { urlScan(tag, mesg); uiFmt( - tag, "\3%d%s\3 leaves, %s%s%s", - color(user), nick, quot, mesg, quot + tag, UI_COLD, + "\3%d%s\3 leaves, \"%s\"", + color(user), nick, dequote(mesg) ); } else { - uiFmt(tag, "\3%d%s\3 leaves", color(user), nick); + uiFmt(tag, UI_COLD, "\3%d%s\3 leaves", color(user), nick); } } } @@ -216,10 +249,12 @@ static void handleReplyTopic(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( - tag, "The sign in \3%d%s\3 reads, \"%s\"", + tag, UI_COLD, + "The sign in \3%d%s\3 reads, \"%s\"", color(chan), chan, topic ); } @@ -228,11 +263,14 @@ 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( - tag, "\3%d%s\3 places a new sign in \3%d%s\3, \"%s\"", + tag, UI_COLD, + "\3%d%s\3 places a new sign in \3%d%s\3, \"%s\"", color(user), nick, color(chan), chan, topic ); } @@ -256,7 +294,10 @@ static void handleReplyWho(char *prefix, char *params) { params, 6, 0, NULL, &chan, &user, NULL, NULL, &nick ); struct Tag tag = tagFor(chan); + + // FIXME: Don't clobber order if they already exist. tabTouch(tag, nick); + size_t cap = sizeof(who.buf) - who.len; int len = snprintf( &who.buf[who.len], cap, @@ -270,8 +311,10 @@ static void handleReplyEndOfWho(char *prefix, char *params) { char *chan; shift(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &chan); struct Tag tag = tagFor(chan); + uiFmt( - tag, "In \3%d%s\3 are %s", + tag, UI_COLD, + "In \3%d%s\3 are %s", color(chan), chan, who.buf ); who.len = 0; @@ -280,12 +323,16 @@ static void handleReplyEndOfWho(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); + struct Tag tag; while (TAG_NONE.id != (tag = tabTag(prev)).id) { tabReplace(tag, prev, next); + uiFmt( - tag, "\3%d%s\3 is now known as \3%d%s\3", + tag, UI_COLD, + "\3%d%s\3 is now known as \3%d%s\3", color(user), prev, color(user), next ); } @@ -296,11 +343,15 @@ static void handleCTCP(struct Tag tag, char *nick, char *user, char *mesg) { char *ctcp = strsep(&mesg, " "); char *params = strsep(&mesg, "\1"); if (strcmp(ctcp, "ACTION")) return; - if (!isSelf(nick, user)) tabTouch(tag, nick); + + bool self = isSelf(nick, user); + if (!self) tabTouch(tag, nick); + urlScan(tag, params); - bool ping = isPing(nick, user, params); + bool ping = !self && isPing(params); uiFmt( - tag, "%c\3%d* %s\17 %s", + tag, (ping ? UI_HOT : UI_WARM), + "%c\3%d* %s\17 %s", ping["\17\26"], color(user), nick, params ); } @@ -313,12 +364,15 @@ static void handlePrivmsg(char *prefix, char *params) { handleCTCP(tag, nick, user, mesg); return; } - if (!isSelf(nick, user)) tabTouch(tag, nick); - urlScan(tag, mesg); + bool self = isSelf(nick, user); - bool ping = isPing(nick, user, mesg); + if (!self) tabTouch(tag, nick); + + urlScan(tag, mesg); + bool ping = !self && isPing(mesg); uiFmt( - tag, "%c\3%d%c%s%c\17 %s", + tag, (ping ? UI_HOT : UI_WARM), + "%c\3%d%c%s%c\17 %s", ping["\17\26"], color(user), self["<("], nick, self[">)"], mesg ); } @@ -328,11 +382,16 @@ static void handleNotice(char *prefix, char *params) { shift(prefix, &nick, &user, NULL, params, 2, 0, &chan, &mesg); struct Tag tag = TAG_STATUS; if (user) tag = (strcmp(chan, self.nick) ? tagFor(chan) : tagFor(nick)); - if (!isSelf(nick, user)) tabTouch(tag, nick); + + bool self = isSelf(nick, user); + if (!self) tabTouch(tag, nick); + urlScan(tag, mesg); + bool ping = !self && isPing(mesg); uiFmt( - tag, "\3%d-%s-\3 %s", - color(user), nick, mesg + tag, (ping ? UI_HOT : UI_WARM), + "%c\3%d-%s-\17 %s", + ping["\17\26"], color(user), nick, mesg ); } diff --git a/input.c b/input.c index 997c570..cb0a12c 100644 --- a/input.c +++ b/input.c @@ -40,7 +40,7 @@ static void privmsg(struct Tag tag, bool action, const char *mesg) { static char *param(const char *command, char **params, const char *name) { char *param = strsep(params, " "); if (param) return param; - uiFmt(TAG_STATUS, "%s requires a %s", command, name); + uiFmt(TAG_STATUS, UI_WARM, "%s requires a %s", command, name); return NULL; } @@ -123,7 +123,7 @@ static void inputView(struct Tag tag, char *params) { if (tag.id != TAG_NONE.id) { uiViewTag(tag); } else { - uiFmt(TAG_STATUS, "No view for %s", view); + uiFmt(TAG_STATUS, UI_WARM, "No view for %s", view); } } } @@ -178,7 +178,7 @@ void input(struct Tag tag, char *input) { COMMANDS[i].handler(tag, input); return; } - uiFmt(TAG_STATUS, "%s isn't a recognized command", command); + uiFmt(TAG_STATUS, UI_WARM, "%s isn't a recognized command", command); } void inputTab(void) { diff --git a/irc.c b/irc.c index 0a131b2..fa858b4 100644 --- a/irc.c +++ b/irc.c @@ -106,7 +106,12 @@ void ircFmt(const char *format, ...) { int len = vasprintf(&buf, format, ap); va_end(ap); if (!buf) err(EX_OSERR, "vasprintf"); - if (self.verbose) uiFmt(TAG_VERBOSE, "\3%d<<<\3 %.*s", IRC_WHITE, len - 2, buf); + if (self.verbose) { + uiFmt( + TAG_VERBOSE, UI_COLD, + "\3%d<<<\3 %.*s", IRC_WHITE, len - 2, buf + ); + } ircWrite(buf, len); free(buf); } @@ -126,7 +131,12 @@ void ircRead(void) { char *crlf, *line = buf; while ((crlf = strnstr(line, "\r\n", &buf[len] - line))) { crlf[0] = '\0'; - if (self.verbose) uiFmt(TAG_VERBOSE, "\3%d>>>\3 %s", IRC_GRAY, line); + if (self.verbose) { + uiFmt( + TAG_VERBOSE, UI_COLD, + "\3%d>>>\3 %s", IRC_GRAY, line + ); + } handle(line); line = &crlf[2]; } diff --git a/ui.c b/ui.c index 29b7b08..b4e48ba 100644 --- a/ui.c +++ b/ui.c @@ -75,6 +75,8 @@ struct View { WINDOW *topic; WINDOW *log; int scroll; + int unread; + bool hot; bool mark; struct View *prev; struct View *next; @@ -145,6 +147,15 @@ static void viewClose(struct View *view) { free(view); } +static void viewMark(struct View *view) { + view->mark = true; +} +static void viewUnmark(struct View *view) { + view->unread = 0; + view->hot = false; + view->mark = false; +} + static struct { bool hide; struct View *view; @@ -233,8 +244,8 @@ static void uiView(struct View *view) { termTitle(view->tag.name); if (view->topic) touchwin(view->topic); touchwin(view->log); - ui.view->mark = true; - view->mark = false; + viewMark(ui.view); + viewUnmark(view); ui.view = view; } @@ -383,37 +394,40 @@ void uiTopic(struct Tag tag, const char *topic) { free(wcs); } -void uiLog(struct Tag tag, const wchar_t *line) { +void uiLog(struct Tag tag, enum UIHeat heat, const wchar_t *line) { struct View *view = viewTag(tag); waddch(view->log, '\n'); - if (view->mark) { - waddch(view->log, '\n'); - view->mark = false; + if (view->mark && heat > UI_COLD) { + if (!view->unread++) waddch(view->log, '\n'); + if (heat > UI_WARM) { + view->hot = true; + beep(); // TODO: Notification. + } } addIRC(view->log, line); } -void uiFmt(struct Tag tag, const wchar_t *format, ...) { +void uiFmt(struct Tag tag, enum UIHeat heat, const wchar_t *format, ...) { wchar_t *wcs; va_list ap; va_start(ap, format); vaswprintf(&wcs, format, ap); va_end(ap); if (!wcs) err(EX_OSERR, "vaswprintf"); - uiLog(tag, wcs); + uiLog(tag, heat, wcs); free(wcs); } static void logScrollUp(int lines) { int height = logHeight(ui.view); if (ui.view->scroll == height) return; - if (ui.view->scroll == LOG_LINES) ui.view->mark = true; + if (ui.view->scroll == LOG_LINES) viewMark(ui.view); ui.view->scroll = MAX(ui.view->scroll - lines, height); } static void logScrollDown(int lines) { if (ui.view->scroll == LOG_LINES) return; ui.view->scroll = MIN(ui.view->scroll + lines, LOG_LINES); - if (ui.view->scroll == LOG_LINES) ui.view->mark = false; + if (ui.view->scroll == LOG_LINES) viewUnmark(ui.view); } static void logPageUp(void) { logScrollUp(logHeight(ui.view) / 2); @@ -426,8 +440,8 @@ static bool keyChar(wchar_t ch) { if (iswascii(ch)) { enum TermEvent event = termEvent((char)ch); switch (event) { - break; case TERM_FOCUS_IN: ui.view->mark = false; - break; case TERM_FOCUS_OUT: ui.view->mark = true; + break; case TERM_FOCUS_IN: viewUnmark(ui.view); + break; case TERM_FOCUS_OUT: viewMark(ui.view); break; default: {} } if (event) return false;