Become multi-channel
There's a lot of UI missing for it, but it technically works.master
parent
e9793b4bce
commit
07c750d25c
2
Makefile
2
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
|
||||
|
||||
|
|
9
README
9
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
|
||||
|
|
43
chat.c
43
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);
|
||||
|
|
102
chat.h
102
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
|
||||
|
|
139
edit.c
139
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;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
break; case EDIT_INSERT: accept(); insert(ch);
|
||||
break; case EDIT_BACKSPACE: reject(); backspace();
|
||||
break; case EDIT_DELETE: reject(); delete();
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
|
161
handle.c
161
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 <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);
|
||||
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");
|
||||
}
|
||||
ircFmt("JOIN %s\r\n", chat.join);
|
||||
|
||||
static void handle372(char *prefix, char *params) {
|
||||
char *mesg;
|
||||
shift(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &mesg);
|
||||
if (mesg[0] == '-' && mesg[1] == ' ') mesg = &mesg[2];
|
||||
uiFmt(TAG_DEFAULT, "%s", mesg);
|
||||
}
|
||||
|
||||
static void handleJoin(char *prefix, char *params) {
|
||||
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
79
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);
|
||||
}
|
||||
}
|
||||
|
|
13
irc.c
13
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];
|
||||
}
|
||||
|
|
35
tab.c
35
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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
217
ui.c
217
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 (ch == L'\33') {
|
||||
esc = true;
|
||||
return false;
|
||||
}
|
||||
if (esc && ch == L'[') {
|
||||
esc = false;
|
||||
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 (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;
|
||||
csi = false;
|
||||
return update;
|
||||
}
|
||||
|
||||
static bool keyCode(wint_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 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 false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool keyCode(wchar_t ch) {
|
||||
switch (ch) {
|
||||
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 true;
|
||||
}
|
||||
|
||||
void uiRead(void) {
|
||||
|
|
48
url.c
48
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 };
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue