Become multi-channel

There's a lot of UI missing for it, but it technically works.
weechat-hashes
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
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

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.
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

43
chat.c
View File

@ -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);

102
chat.h
View File

@ -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

141
edit.c
View File

@ -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;
}

163
handle.c
View File

@ -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 <name> 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 <name> 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 },

79
input.c
View File

@ -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(&params, " ");
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(&params, " ");
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(&params, "-,"), NULL, 0);
if (!params) { urlOpen(from); return; }
if (!params) { urlOpen(tag, from); return; }
size_t to = strtoul(strsep(&params, "-,"), 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(&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 {
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);
}
}

13
irc.c
View File

@ -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];
}

35
tab.c
View File

@ -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) {

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;
}
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) {

50
url.c
View File

@ -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;
}
}