diff --git a/.github/kirc.png b/.github/kirc.png
deleted file mode 100644
index 260e13fb..00000000
Binary files a/.github/kirc.png and /dev/null differ
diff --git a/.github/tty.gif b/.github/tty.gif
deleted file mode 100644
index ed381f57..00000000
Binary files a/.github/tty.gif and /dev/null differ
diff --git a/README b/README
index 65f752fd..571d8b62 100644
--- a/README
+++ b/README
@@ -6,7 +6,7 @@
-->
-
+
KISS for IRC, a tiny IRC client written in POSIX C99.
@@ -19,7 +19,7 @@
-
+
## Features
@@ -27,8 +27,9 @@
* Excellent cross-platform compatibility.
* Asynchronous user input and server messager handling.
* No dependencies other than a C99 compiler.
-* Native SASL PLAIN and EXTERNAL authentication support.
-* TLS/SSL protocol capable (via external TLS utilities).
+* Simple Authentication and Security Layer (SASL) procotol support.
+* Client-to-client protocol (CTCP) support.
+* Transport Layer Security (TLS) protocol support (via external utilities).
* Full chat history logging.
* Multi-channel joining at server connection.
* Simple command aliases and full support for all RFC 2812 commands.
@@ -65,16 +66,17 @@ make install
Consult `man kirc` for a full list and explanation of available `kirc` arguments.
```shell
-kirc [-s hostname] [-p port] [-c channels] [-n nickname] [-r realname] [-u username] [-k password] [-a token] [-x command] [-w nick_width] [-o logfile] [-e|v|V]
+kirc [-s hostname] [-p port] [-c channels] [-n nickname] [-r realname] [-u username] [-k password] [-a token] [-x command] [-o logfile] [-e|v|V]
```
### Command Aliases
```shell
- Send a PRIVMSG to the current channel.
-@ Send a message to a specified channel or nick
-/ Send command to IRC server (see RFC 2812 for full list).
-/# Assign new default message channel.
+ Send a PRIVMSG to the current channel.
+@ Send a message to a specified channel or nick
+@@ Send a CTCP ACTION message to a specified channel or nick
+/ Send command to IRC server (see RFC 2812 for full list).
+/# Assign new default message channel.
```
### User Input Key Bindings
@@ -86,7 +88,10 @@ kirc [-s hostname] [-p port] [-c channels] [-n nickname] [-r realname] [-u usern
* **CTRL+W** deletes the previous word.
* **CTRL+U** deletes the entire line.
* **CTRL+K** deletes the from current character to end of line.
-* **CTRL+C** Force quit kirc
+* **CTRL+C** Force quit kirc.
+* **CTRL+D** deletes the character to the right of cursor.
+* **CTRL+T** swap character at cursor with previous character.
+* **CTRL+H** equivalent to backspace.
## Support Documentation
diff --git a/kirc.1 b/kirc.1
index 16bd7497..85cd79bf 100644
--- a/kirc.1
+++ b/kirc.1
@@ -41,9 +41,6 @@ Specifies the USER connection username
.BI \-k " pass"
Specifies the PASS connection password
.TP
-.BI \-w " nick_width"
-Specifies max character width printed in the left column
-.TP
.BI \-x " command"
Specifies additional commands to send to the host after initial connection.
.TP
diff --git a/kirc.c b/kirc.c
index ea3c3e54..c83ae573 100644
--- a/kirc.c
+++ b/kirc.c
@@ -5,6 +5,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -12,32 +13,46 @@
#include
#include
-#define VERSION "0.1.8" /* version */
-#define MSG_MAX 512 /* max message length */
-#define CHA_MAX 200 /* max channel length */
-
-static int conn; /* connection socket */
-static char cdef[MSG_MAX] = "?"; /* default PRIVMSG channel */
-static int verb = 0; /* verbose output */
-static int sasl = 0; /* SASL method */
-static size_t gutl = 20; /* max printed nick chars */
-static char * host = "irc.freenode.org"; /* host address */
-static char * port = "6667"; /* port */
-static char * chan = NULL; /* channel(s) */
-static char * nick = NULL; /* nickname */
-static char * pass = NULL; /* password */
-static char * user = NULL; /* user name */
-static char * auth = NULL; /* PLAIN SASL token */
-static char * real = NULL; /* real name */
-static char * olog = NULL; /* chat log path*/
-static char * inic = NULL; /* additional server command */
-
-static struct termios orig; /* restore at exit. */
-static int rawmode = 0; /* check if restore is needed */
-static int atexit_registered = 0; /* register atexit() */
+#define VERSION "0.1.9" /* version */
+#define AUTHORS "Michael Czigler" /* authors */
+#define MSG_MAX 512 /* max message length */
+#define CHA_MAX 200 /* max channel length */
+#define NIC_MAX 26 /* max nickname length */
+#define CTCP_CMDS "ACTION VERSION TIME CLIENTINFO PING"
+
+static char cdef[MSG_MAX] = "?"; /* default PRIVMSG channel */
+static int conn; /* connection socket */
+static int verb = 0; /* verbose output */
+static int sasl = 0; /* SASL method */
+static char * host = "irc.freenode.org"; /* host address */
+static char * port = "6667"; /* port */
+static char * chan = NULL; /* channel(s) */
+static char * nick = NULL; /* nickname */
+static char * pass = NULL; /* password */
+static char * user = NULL; /* user name */
+static char * auth = NULL; /* PLAIN SASL token */
+static char * real = NULL; /* real name */
+static char * olog = NULL; /* chat log path*/
+static char * inic = NULL; /* additional server command */
+
+struct Param {
+ char * prefix;
+ char * suffix;
+ char * message;
+ char * nickname;
+ char * command;
+ char * channel;
+ size_t offset;
+ size_t maxcols;
+ int nicklen;
+};
+
+static struct termios orig; /* restore at exit. */
+static int rawmode = 0; /* check if restore is needed */
+static int atexit_registered = 0; /* register atexit() */
struct State {
- char *buf; /* Edited line buffer. */
+ char * buf; /* Edited line buffer. */
size_t buflen; /* Edited line buffer size. */
const char *prompt; /* Prompt to display. */
size_t plen; /* Prompt length. */
@@ -88,16 +103,13 @@ static int getCursorPosition(int ifd, int ofd) {
char buf[32];
int cols, rows;
unsigned int i = 0;
- /* Report cursor location */
if (write(ofd, "\x1b[6n", 4) != 4) return -1;
- /* Read the response: ESC [ rows ; cols R */
while (i < sizeof(buf)-1) {
if (read(ifd,buf+i,1) != 1) break;
if (buf[i] == 'R') break;
i++;
}
buf[i] = '\0';
- /* Parse it. */
if (buf[0] != 27 || buf[1] != '[') return -1;
if (sscanf(buf+2, "%d;%d", &rows, &cols) != 2) return -1;
return cols;
@@ -108,15 +120,15 @@ static int getColumns(int ifd, int ofd) {
if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {
int start, cols;
- start = getCursorPosition(ifd,ofd);
+ start = getCursorPosition(ifd, ofd);
if (start == -1) return 80;
if (write(ofd,"\x1b[999C",6) != 6) return 80;
- cols = getCursorPosition(ifd,ofd);
+ cols = getCursorPosition(ifd, ofd);
if (cols == -1) return 80;
if (cols > start) {
char seq[32];
- snprintf(seq, sizeof(seq), "\x1b[%dD",cols-start);
- if (write(ofd,seq,strlen(seq)) == -1) {} /* Can't recover from write error. */
+ snprintf(seq, sizeof(seq), "\x1b[%dD", cols - start);
+ if (write(ofd, seq, strlen(seq)) == -1) {}
}
return cols;
} else {
@@ -130,10 +142,9 @@ static void abInit(struct abuf *ab) {
}
static void abAppend(struct abuf *ab, const char *s, int len) {
- char *new = realloc(ab->b,ab->len+len);
-
+ char *new = realloc(ab->b, ab->len + len);
if (new == NULL) return;
- memcpy(new+ab->len,s,len);
+ memcpy(new + ab->len, s, len);
ab->b = new;
ab->len += len;
}
@@ -164,12 +175,12 @@ static void refreshLine(struct State *l) {
size_t pos = l->pos;
struct abuf ab;
- while((plen+pos) >= l->cols) {
+ while (plen + pos >= l->cols) {
buf++;
len--;
pos--;
}
- while (plen+len > l->cols) {
+ while (plen + len > l->cols) {
len--;
}
@@ -182,7 +193,7 @@ static void refreshLine(struct State *l) {
abAppend(&ab, seq, strlen(seq));
snprintf(seq, sizeof(seq), "\r\x1b[%dC", (int)(pos + plen));
abAppend(&ab, seq, strlen(seq));
- if (write(fd, ab.b, ab.len) == -1) {} /* Can't recover from write error. */
+ if (write(fd, ab.b, ab.len) == -1) {}
abFree(&ab);
}
@@ -391,7 +402,7 @@ static void raw(char *fmt, ...) {
if (verb) printf("<< %s", cmd_str);
if (olog) logAppend(cmd_str, olog);
- if (write(conn, cmd_str, strlen(cmd_str)) < 0) {
+ if (write(conn, cmd_str, strnlen(cmd_str, MSG_MAX)) < 0) {
perror("write");
exit(EXIT_FAILURE);
}
@@ -399,7 +410,7 @@ static void raw(char *fmt, ...) {
free(cmd_str);
}
-static int connectionInit(void) {
+static int initConnection(void) {
int gai_status;
struct addrinfo *res, hints = {
.ai_family = AF_UNSPEC,
@@ -439,16 +450,17 @@ static int connectionInit(void) {
return 0;
}
-static void messageWrap(char *line, size_t offset) {
- char *tok;
- size_t cmax = getColumns(STDIN_FILENO, STDOUT_FILENO);
- size_t wordwidth, spaceleft = cmax - gutl - offset, spacewidth = 1;
-
- for (tok = strtok(line, " "); tok != NULL; tok = strtok(NULL, " ")) {
- wordwidth = strlen(tok);
+static void messageWrap(struct Param *p) {
+ if (!p->message)
+ return;
+ char * tok;
+ size_t wordwidth, spacewidth = 1;
+ size_t spaceleft = p->maxcols - p->nicklen - p->offset;
+ for (tok = strtok(p->message, " "); tok != NULL; tok = strtok(NULL, " ")) {
+ wordwidth = strnlen(tok, MSG_MAX);
if ((wordwidth + spacewidth) > spaceleft) {
- printf("\r\n%*.s%s ", (int) gutl + 1, " ", tok);
- spaceleft = cmax - (gutl + 1);
+ printf("\r\n%*.s%s ", (int) p->nicklen + 1, " ", tok);
+ spaceleft = p->maxcols - (p->nicklen + 1);
} else {
printf("%s ", tok);
}
@@ -456,6 +468,74 @@ static void messageWrap(char *line, size_t offset) {
}
}
+static void paramPrintNick(struct Param *p) {
+ printf("\x1b[35;1m%*s\x1b[0m ", p->nicklen - 4, p->nickname);
+ printf("--> \x1b[35;1m%s\x1b[0m", p->message);
+}
+
+static void paramPrintPart(struct Param *p) {
+ printf("%*s<-- \x1b[34;1m%s\x1b[0m", p->nicklen - 3, "", p->nickname);
+ if (p->channel != NULL && strstr(p->channel, cdef) == NULL)
+ printf(" [\x1b[33m%s\x1b[0m] ", p->channel);
+}
+
+static void paramPrintQuit(struct Param *p) {
+ printf("%*s<<< \x1b[34;1m%s\x1b[0m", p->nicklen - 3, "", p->nickname);
+}
+
+static void paramPrintJoin(struct Param *p) {
+ printf("%*s--> \x1b[32;1m%s\x1b[0m", p->nicklen - 3, "", p->nickname);
+ if (p->channel != NULL && strstr(p->channel, cdef) == NULL)
+ printf(" [\x1b[33m%s\x1b[0m] ", p->channel);
+}
+
+static void handleCTCP(const char *nickname, char *message) {
+ if (message[0] != '\001' && strncmp(message, "ACTION", 6))
+ return;
+ message++;
+ if (!strncmp(message, "VERSION", 7)) {
+ raw("NOTICE %s :\001VERSION kirc " VERSION "\001\r\n", nickname);
+ } else if (!strncmp(message, "TIME", 7)) {
+ char buf[256] = {0};
+ time_t rawtime = time(NULL);
+ struct tm *ptm = localtime(&rawtime);
+ strftime(buf, 256, "%c", ptm);
+ if (ptm) raw("NOTICE %s :\001TIME %s\001\r\n", nickname, buf);
+ } else if (!strncmp(message, "CLIENTINFO", 10)) {
+ raw("NOTICE %s :\001CLIENTINFO " CTCP_CMDS "\001\r\n", nickname);
+ } else if (!strncmp(message, "PING", 4)) {
+ raw("NOTICE %s :\001%s\r\n", nickname, message);
+ }
+}
+
+static void paramPrintPriv(struct Param *p) {
+ int s = 0;
+ if (strnlen(p->nickname, MSG_MAX) <= p->nicklen)
+ s = p->nicklen - strnlen(p->nickname, MSG_MAX);
+ if (p->channel != NULL && strcmp(p->channel, nick) == 0) {
+ handleCTCP(p->nickname, p->message);
+ printf("%*s\x1b[33;1m%-.*s\x1b[36m ", s, "", p->nicklen, p->nickname);
+ } else if (p->channel != NULL && strcmp(p->channel + 1, cdef)) {
+ printf("%*s\x1b[33;1m%-.*s\x1b[0m", s, "", p->nicklen, p->nickname);
+ printf(" [\x1b[33m%s\x1b[0m] ", p->channel);
+ p->offset += 12 + strnlen(p->channel, CHA_MAX);
+ } else {
+ printf("%*s\x1b[33;1m%-.*s\x1b[0m ", s, "", p->nicklen, p->nickname);
+ }
+ if (!strncmp(p->message, "\x01""ACTION", 7)) {
+ p->message += 7;
+ p->offset += 10;
+ printf("[ACTION] ");
+ }
+}
+
+static void paramPrintChan(struct Param *p) {
+ int s = 0;
+ if (strnlen(p->nickname, MSG_MAX) <= p->nicklen)
+ s = p->nicklen - strnlen(p->nickname, MSG_MAX);
+ printf("%*s\x1b[33;1m%-.*s\x1b[0m ", s, "", p->nicklen, p->nickname);
+}
+
static void rawParser(char *string) {
if (!strncmp(string, "PING", 4)) {
string[1] = 'O';
@@ -470,49 +550,37 @@ static void rawParser(char *string) {
if (verb) printf(">> %s", string);
if (olog) logAppend(string, olog);
- char *tok;
- char *prefix = strtok(string, " ") + 1;
- char *suffix = strtok(NULL, ":");
- char *message = strtok(NULL, "\r");
- char *nickname = strtok(prefix, "!");
- char *command = strtok(suffix, "#& ");
- char *channel = strtok(NULL, " \r");
- int g = gutl;
- int s = gutl - (strlen(nickname) <= gutl ? strlen(nickname) : gutl);
- size_t offset = 0;
-
- if (!strncmp(command, "001", 3) && chan != NULL) {
+ char * tok;
+ struct Param p;
+ p.prefix = strtok(string, " ") + 1;
+ p.suffix = strtok(NULL, ":");
+ p.message = strtok(NULL, "\r");
+ p.nickname = strtok(p.prefix, "!");
+ p.command = strtok(p.suffix, "#& ");
+ p.channel = strtok(NULL, " \r");
+ p.maxcols = getColumns(STDIN_FILENO, STDOUT_FILENO);
+ p.nicklen = (p.maxcols / 3 > NIC_MAX ? NIC_MAX : p.maxcols / 3);
+ p.offset = 0;
+
+ if (!strncmp(p.command, "001", 3) && chan != NULL) {
for (tok = strtok(chan, ",|"); tok != NULL; tok = strtok(NULL, ",|")) {
strcpy(cdef, tok);
raw("JOIN #%s\r\n", tok);
} return;
- } else if (!strncmp(command, "QUIT", 4)) {
- printf("%*s<<< \x1b[34;1m%s\x1b[0m", g - 3, "", nickname);
- } else if (!strncmp(command, "PART", 4)) {
- printf("%*s<-- \x1b[34;1m%s\x1b[0m", g - 3, "", nickname);
- if (channel != NULL && strstr(channel, cdef) == NULL) {
- printf(" [\x1b[33m%s\x1b[0m] ", channel);
- }
- } else if (!strncmp(command, "JOIN", 4)) {
- printf("%*s--> \x1b[32;1m%s\x1b[0m", g - 3, "", nickname);
- if (channel != NULL && strstr(channel, cdef) == NULL) {
- printf(" [\x1b[33m%s\x1b[0m] ", channel);
- }
- } else if (!strncmp(command, "NICK", 4)) {
- printf("\x1b[35;1m%*s\x1b[0m ", g - 4, nickname);
- printf("--> \x1b[35;1m%s\x1b[0m", message);
- } else if (!strncmp(command, "PRIVMSG", 7)) {
- if (channel != NULL && strcmp(channel, nick) == 0) {
- printf("%*s\x1b[33;1m%-.*s\x1b[36m ", s, "", g, nickname);
- } else if (channel != NULL && strstr(channel, cdef) == NULL) {
- printf("%*s\x1b[33;1m%-.*s\x1b[0m", s, "", g, nickname);
- printf(" [\x1b[33m%s\x1b[0m] ", channel);
- offset += 12 + strlen(channel);
- } else printf("%*s\x1b[33;1m%-.*s\x1b[0m ", s, "", g, nickname);
- messageWrap((message ? message : " "), offset);
+ } else if (!strncmp(p.command, "QUIT", 4)) {
+ paramPrintQuit(&p);
+ } else if (!strncmp(p.command, "PART", 4)) {
+ paramPrintPart(&p);
+ } else if (!strncmp(p.command, "JOIN", 4)) {
+ paramPrintJoin(&p);
+ } else if (!strncmp(p.command, "NICK", 4)) {
+ paramPrintNick(&p);
+ } else if (!strncmp(p.command, "PRIVMSG", 7)) {
+ paramPrintPriv(&p);
+ messageWrap(&p);
} else {
- printf("%*s\x1b[33;1m%-.*s\x1b[0m ", s, "", g, nickname);
- messageWrap((message ? message : " "), offset);
+ paramPrintChan(&p);
+ messageWrap(&p);
}
printf("\x1b[0m\r\n");
}
@@ -568,50 +636,56 @@ static void handleUserInput(char *usrin) {
}
printf("\r\x1b[0K");
- if (usrin[0] == '/' && usrin[1] == '#') {
- strcpy(cdef, usrin + 2);
- printf("\x1b[35mnew channel: #%s\x1b[0m", cdef);
- } else if (usrin[0] == '/') {
- raw("%s\r\n", usrin + 1);
- printf("\x1b[35m%s\x1b[0m", usrin);
- } else if (usrin[0] == '@') {
- strtok_r(usrin, " ", &tok);
- raw("privmsg %s :%s\r\n", usrin + 1, tok);
- printf("\x1b[35mprivmsg %s :%s\x1b[0m", usrin + 1, tok);
- } else {
- raw("privmsg #%s :%s\r\n", cdef, usrin);
- printf("\x1b[35mprivmsg #%s :%s\x1b[0m", cdef, usrin);
+ switch (usrin[0]) {
+ case '/' : /* send system command */
+ if (usrin[1] == '#') {
+ strcpy(cdef, usrin + 2);
+ printf("\x1b[35mnew channel: #%s\x1b[0m\r\n", cdef);
+ } else {
+ raw("%s\r\n", usrin + 1);
+ printf("\x1b[35m%s\x1b[0m\r\n", usrin);
+ }
+ break;
+ case '@' : /* send private message to target channel or user */
+ strtok_r(usrin, " ", &tok);
+ if (usrin[1] == '@') {
+ raw("privmsg %s :\001ACTION %s\001\r\n", usrin + 2, tok);
+ printf("\x1b[35mprivmsg %s :ACTION %s\x1b[0m\r\n", usrin + 2, tok);
+ } else {
+ raw("privmsg %s :%s\r\n", usrin + 1, tok);
+ printf("\x1b[35mprivmsg %s :%s\x1b[0m\r\n", usrin + 1, tok);
+ } break;
+ default : /* send private message to default channel */
+ raw("privmsg #%s :%s\r\n", cdef, usrin);
+ printf("\x1b[35mprivmsg #%s :%s\x1b[0m\r\n", cdef, usrin);
}
- printf("\r\n");
}
static void usage(void) {
fputs("kirc [-s host] [-p port] [-c channel] [-n nick] [-r realname] \
-[-u username] [-k password] [-a token] [-x command] [-w nickwidth] [-o path] \
-[-e|v|V]\n", stderr);
+[-u username] [-k password] [-a token] [-x command] [-o path] [-e|v|V]\n", stderr);
exit(EXIT_FAILURE);
}
int main(int argc, char **argv) {
int cval;
- while ((cval = getopt(argc, argv, "s:p:o:n:k:c:u:r:x:w:a:evV")) != -1) {
+ while ((cval = getopt(argc, argv, "s:p:o:n:k:c:u:r:x:a:evV")) != -1) {
switch (cval) {
- case 'V' : ++verb; break;
- case 'e' : ++sasl; break;
- case 's' : host = optarg; break;
- case 'p' : port = optarg; break;
- case 'r' : real = optarg; break;
- case 'u' : user = optarg; break;
- case 'a' : auth = optarg; break;
- case 'o' : olog = optarg; break;
- case 'n' : nick = optarg; break;
- case 'k' : pass = optarg; break;
- case 'c' : chan = optarg; break;
- case 'x' : inic = optarg; break;
- case 'w' : gutl = atoi(optarg); break;
- case 'v' : puts("kirc-" VERSION); break;
- case '?' : usage(); break;
+ case 'v' : puts("kirc-" VERSION " © 2020 " AUTHORS); break;
+ case 'V' : ++verb; break;
+ case 'e' : ++sasl; break;
+ case 's' : host = optarg; break;
+ case 'p' : port = optarg; break;
+ case 'r' : real = optarg; break;
+ case 'u' : user = optarg; break;
+ case 'a' : auth = optarg; break;
+ case 'o' : olog = optarg; break;
+ case 'n' : nick = optarg; break;
+ case 'k' : pass = optarg; break;
+ case 'c' : chan = optarg; break;
+ case 'x' : inic = optarg; break;
+ case '?' : usage(); break;
}
}
@@ -620,7 +694,7 @@ int main(int argc, char **argv) {
usage();
}
- if (connectionInit() != 0) {
+ if (initConnection() != 0) {
return EXIT_FAILURE;
}