diff options
Diffstat (limited to '')
-rw-r--r-- | .github/example.png | bin | 672695 -> 0 bytes | |||
-rw-r--r-- | .github/example2.png | bin | 964934 -> 0 bytes | |||
-rw-r--r-- | README.md | 35 | ||||
-rw-r--r-- | kirc.c | 444 |
4 files changed, 389 insertions, 90 deletions
diff --git a/.github/example.png b/.github/example.png Binary files differdeleted file mode 100644 index 93ac0f4..0000000 --- a/.github/example.png +++ /dev/null diff --git a/.github/example2.png b/.github/example2.png Binary files differdeleted file mode 100644 index fe654fa..0000000 --- a/.github/example2.png +++ /dev/null @@ -29,16 +29,7 @@ * [TLS/SSL](https://en.m.wikipedia.org/wiki/Transport_Layer_Security) protocol capable (via external TLS utilities). * Full chat history logging. * Multi-channel joining at server connection. -* Simple shortcut commands and full support for all IRC commands in the [RFC 2812](https://tools.ietf.org/html/rfc2812) standard: - -```shell -<message> Send a PRIVMSG to the current channel. -@<channel|nick> <message> Send a message to a specified channel or nick -/<command> Send command to IRC server (see RFC 2812 for full list). -/#<channel> Assign new default message channel. -/? Print current message channel. -``` - +* Simple command aliases and full support for all [RFC 2812](https://tools.ietf.org/html/rfc2812) commands. * Color scheme definition via [ANSI 8-bit colors](https://en.wikipedia.org/wiki/ANSI_escape_code), allowing for uniform color definition across all shell applications. ## Installation & Usage @@ -75,6 +66,26 @@ Consult `man kirc` for a full list and explanation of available `kirc` arguments 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] ``` +### Command Aliases + +```shell +<message> Send a PRIVMSG to the current channel. +@<channel|nick> <message> Send a message to a specified channel or nick +/<command> Send command to IRC server (see RFC 2812 for full list). +/#<channel> Assign new default message channel. +/? Print current message channel. +``` + +### User Input Key Bindings + +* **CTRL+B** or **LEFT ARROW** moves the cursor one character to the left. +* **CTRL+F** or **RIGHT ARROW** moves the cursor one character to the right. +* **CTRL+A** moves the cursor to the end of the line. +* **CTRL+E** or **HOME** moves the cursor to the start of the line. +* **CTRL+W** deletes the previous word. +* **CTRL+U** deletes the entire line. +* **CTRL+K** deletes the from current character to end of line. + ## Transport Layer Security (TLS) Support There is no native TLS/SSL support. Instead, users can achieve this functionality by using third-party utilities (e.g. stunnel, [socat](https://linux.die.net/man/1/socat), ghosttunnel, etc). @@ -154,10 +165,6 @@ Applying a new color scheme is easy! One of the quickest ways is to use an appli * **KISS** is an acronym for [Keep It Simple Stupid](https://en.wikipedia.org/wiki/KISS_principle), which is a design principle noted by the U.S. Navy in 1960s. The KISS principle states that most systems work best if they are kept simple rather than made complicated; therefore, simplicity should be a key goal in design, and unnecessary complexity should be avoided. * **POSIX** is an acronym for [Portable Operating System Interface](https://opensource.com/article/19/7/what-posix-richard-stallman-explains), which is a family of standards specified by the IEEE Computer Society for maintaining compatibility between operating systems. The *C99* Standard is preferred over other versions (e.g. *C89* or *C11*) since this currently the only one specified by [POSIX](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/c99.html). -## Known Bugs - -Some users may experience abnormal *BACKSPACE* key press behavior, particularly when trying to return to a previous line being editted. This has been confirmed to be an [upstream issue](https://github.com/mcpcpc/kirc/issues/39) and has been reported accordingly to the known impacted terminal emulators. While we wait for the upstream fixes, I would recommend using [urxvt](https://wiki.archlinux.org/index.php/Rxvt-unicode) or a terminal multiplexer (such as [screen](https://www.gnu.org/software/screen/) or [tmux](https://github.com/tmux/tmux/wiki)), which seemed to have resolved this by setting the [terminfo](https://osr507doc.sco.com/en/man/html.M/terminfo.M.html) `<bw>` flag as default. Alternatively, you can try to set terminfo `<bw>` flag manually by passing the command escape sequence `echo -e "\x1b[?45h"` or using the *tputs* function before starting *kirc*. - ## Contact For any further questions or concerns, feel free to reach out to me, [mcpcpc](https://github.com/mcpcpc), on `#kirc` @@ -13,7 +13,7 @@ #include <sys/socket.h> #include <sys/ioctl.h> -#define VERSION "0.1.3" +#define VERSION "0.1.4" #define MSG_MAX 512 /* max message length */ #define CHA_MAX 200 /* max channel length */ @@ -34,9 +34,326 @@ static char * real = NULL; /* real name */ static char * olog = NULL; /* chat log path*/ static char * inic = NULL; /* additional server command */ -static void -log_append(char *str, char *path) { +static struct termios orig; +static int rawmode = 0; +static int atexit_registered = 0; + +struct State { + int ifd; /* Terminal stdin file descriptor. */ + int ofd; /* Terminal stdout file descriptor. */ + char *buf; /* Edited line buffer. */ + size_t buflen; /* Edited line buffer size. */ + size_t pos; /* Current cursor position. */ + size_t oldpos; /* Previous refresh cursor position. */ + size_t len; /* Current edited line length. */ + size_t cols; /* Number of columns in terminal. */ +}; + +static void disableRawMode() { + if (rawmode && tcsetattr(STDIN_FILENO,TCSAFLUSH,&orig) != -1) + rawmode = 0; +} + +static int enableRawMode(int fd) { + struct termios raw; + + if (!isatty(STDIN_FILENO)) { + errno = ENOTTY; + return -1; + } + if (!atexit_registered) { + atexit(disableRawMode); + atexit_registered = 1; + } + if (tcgetattr(fd,&orig) == -1) { + errno = ENOTTY; + return -1; + } + + raw = orig; + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + raw.c_oflag &= ~(OPOST); + raw.c_cflag |= (CS8); + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + raw.c_cc[VMIN] = 1; + raw.c_cc[VTIME] = 0; + + if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) { + errno = ENOTTY; + return -1; + } + + rawmode = 1; + return 0; +} + +static int getCursorPosition(int ifd, int ofd) { + char buf[32]; + int cols, rows; + unsigned int i = 0; + + if (write(ofd, "\x1b[6n", 4) != 4) return -1; + + while (i < sizeof(buf)-1) { + if (read(ifd,buf+i,1) != 1) break; + if (buf[i] == 'R') break; + i++; + } + buf[i] = '\0'; + + if (buf[0] != 27 || buf[1] != '[') return -1; + if (sscanf(buf+2,"%d;%d",&rows,&cols) != 2) return -1; + return cols; +} + +static int getColumns(int ifd, int ofd) { + struct winsize ws; + + if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) { + int start, cols; + + start = getCursorPosition(ifd,ofd); + if (start == -1) return 80; + + if (write(ofd,"\x1b[999C",6) != 6) return 80; + cols = getCursorPosition(ifd,ofd); + if (cols == -1) return 80; + if (cols > start) { + char seq[32]; + snprintf(seq,32,"\x1b[%dD",cols-start); + if (write(ofd,seq,strlen(seq)) == -1) {} + } + return cols; + } else { + return ws.ws_col; + } +} + +struct abuf { + char *b; + int len; +}; + +static void abInit(struct abuf *ab) { + ab->b = NULL; + ab->len = 0; +} + +static void abAppend(struct abuf *ab, const char *s, int len) { + char *new = realloc(ab->b,ab->len+len); + + if (new == NULL) return; + memcpy(new+ab->len,s,len); + ab->b = new; + ab->len += len; +} + +static void abFree(struct abuf *ab) { + free(ab->b); +} + +static void refreshLine(struct State *l) { + char seq[64]; + int fd = l->ofd; + char *buf = l->buf; + size_t len = l->len; + size_t pos = l->pos; + struct abuf ab; + + while(pos >= l->cols) { + buf++; + len--; + pos--; + } + while (len > l->cols) { + len--; + } + + abInit(&ab); + snprintf(seq,64,"\r"); + abAppend(&ab,seq,strlen(seq)); + abAppend(&ab,buf,len); + snprintf(seq,64,"\x1b[0K"); + abAppend(&ab,seq,strlen(seq)); + snprintf(seq,64,"\r\x1b[%dC", (int)(pos)); + abAppend(&ab,seq,strlen(seq)); + if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */ + abFree(&ab); +} + +static int editInsert(struct State *l, char c) { + if (l->len < l->buflen) { + if (l->len == l->pos) { + l->buf[l->pos] = c; + l->pos++; + l->len++; + l->buf[l->len] = '\0'; + if ((l->len < l->cols)) { + char d = c; + if (write(l->ofd,&d,1) == -1) return -1; + } else { + refreshLine(l); + } + } else { + memmove(l->buf+l->pos+1,l->buf+l->pos,l->len-l->pos); + l->buf[l->pos] = c; + l->len++; + l->pos++; + l->buf[l->len] = '\0'; + refreshLine(l); + } + } + return 0; +} + +static void editMoveLeft(struct State *l) { + if (l->pos > 0) { + l->pos--; + refreshLine(l); + } +} + +static void editMoveRight(struct State *l) { + if (l->pos != l->len) { + l->pos++; + refreshLine(l); + } +} + +static void editMoveHome(struct State *l) { + if (l->pos != 0) { + l->pos = 0; + refreshLine(l); + } +} + +static void editMoveEnd(struct State *l) { + if (l->pos != l->len) { + l->pos = l->len; + refreshLine(l); + } +} + +static void editDelete(struct State *l) { + if (l->len > 0 && l->pos < l->len) { + memmove(l->buf+l->pos,l->buf+l->pos+1,l->len-l->pos-1); + l->len--; + l->buf[l->len] = '\0'; + refreshLine(l); + } +} + +static void editBackspace(struct State *l) { + if (l->pos > 0 && l->len > 0) { + memmove(l->buf+l->pos-1,l->buf+l->pos,l->len-l->pos); + l->pos--; + l->len--; + l->buf[l->len] = '\0'; + refreshLine(l); + } +} + +static void editDeletePrevWord(struct State *l) { + size_t old_pos = l->pos; + size_t diff; + + while (l->pos > 0 && l->buf[l->pos-1] == ' ') + l->pos--; + while (l->pos > 0 && l->buf[l->pos-1] != ' ') + l->pos--; + diff = old_pos - l->pos; + memmove(l->buf+l->pos,l->buf+old_pos,l->len-old_pos+1); + l->len -= diff; + refreshLine(l); +} + +static int edit(char *buf, size_t buflen) +{ + struct State l; + + l.ifd = STDIN_FILENO; + l.ofd = STDOUT_FILENO; + l.buf = buf; + l.buflen = buflen; + l.oldpos = l.pos = 0; + l.len = 0; + l.cols = getColumns(STDIN_FILENO, STDOUT_FILENO); + + l.buf[0] = '\0'; + l.buflen--; /* Make sure there is always space for the nulterm */ + + while(1) { + int nread; + char c, seq[3]; + + nread = read(l.ifd,&c,1); + if (nread <= 0) return l.len; + + switch(c) { + case 3: /* ctrl-c */ + errno = EAGAIN; + return -1; + case 4: + if (l.len > 0) { + editDelete(&l); + } else { + return -1; + } + break; + case 13: return (int)l.len; /* enter */ + case 127: /* backspace */ + case 8: editBackspace(&l); break; /* backspace */ + case 2: editMoveLeft(&l); break; /* ctrl+b */ + case 6: editMoveRight(&l); break; /* ctrl+f */ + case 1: editMoveHome(&l); break; /* ctrl+a */ + case 5: editMoveEnd(&l); break; /* ctrl+e */ + case 23: editDeletePrevWord(&l); break; /* ctrl+w */ + case 27: /* esc sequence */ + if (read(l.ifd,seq,1) == -1) break; + if (read(l.ifd,seq+1,1) == -1) break; + if (seq[0] == '[') { + if (seq[1] >= '0' && seq[1] <= '9') { + if (read(l.ifd,seq+2,1) == -1) break; + if (seq[2] == '~') { + switch(seq[1]) { + case '3': editDelete(&l); break; + } + } + } else { + switch(seq[1]) { + case 'C': editMoveRight(&l); break; + case 'D': editMoveLeft(&l); break; + case 'H': editMoveHome(&l); break; + case 'F': editMoveEnd(&l); break; + } + } + } else if (seq[0] == 'O') { + switch(seq[1]) { + case 'H': editMoveHome(&l); break; + case 'F': editMoveEnd(&l); break; + } + } + break; + case 21: /* Ctrl+u, delete the whole line. */ + buf[0] = '\0'; + l.pos = l.len = 0; + refreshLine(&l); + break; + case 11: /* Ctrl+k, delete from current to end of line. */ + buf[l.pos] = '\0'; + l.len = l.pos; + refreshLine(&l); + break; + default: + if (editInsert(&l,c)) return -1; + break; + } + } + return l.len; +} + +static void logAppend(char *str, char *path) { FILE *out; if ((out = fopen(path, "a")) == NULL) { @@ -48,9 +365,7 @@ log_append(char *str, char *path) { fclose(out); } -static void -raw(char *fmt, ...) { - +static void raw(char *fmt, ...) { va_list ap; char *cmd_str = malloc(MSG_MAX); @@ -64,7 +379,7 @@ raw(char *fmt, ...) { va_end(ap); if (verb) printf("<< %s", cmd_str); - if (olog) log_append(cmd_str, olog); + if (olog) logAppend(cmd_str, olog); if (write(conn, cmd_str, strlen(cmd_str)) < 0) { perror("write"); exit(EXIT_FAILURE); @@ -73,9 +388,7 @@ raw(char *fmt, ...) { free(cmd_str); } -static int -connection_initialize(void) { - +static int connectionInit(void) { int gai_status; struct addrinfo *res, hints = { .ai_family = AF_UNSPEC, @@ -115,18 +428,10 @@ connection_initialize(void) { return 0; } -static void -message_wrap(char *line, size_t offset) { - - struct winsize window_dims; - - if (ioctl(0, TIOCGWINSZ, &window_dims) < 0) { - perror("ioctrl"); - exit(EXIT_FAILURE); - } +static void messageWrap(char *line, size_t offset) { - unsigned short cmax = window_dims.ws_col; 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, " ")) { @@ -140,12 +445,10 @@ message_wrap(char *line, size_t offset) { spaceleft -= (wordwidth + spacewidth); } - putchar('\n'); + puts("\x1b[0m"); } -static void -raw_parser(char *string) { - +static void rawParser(char *string) { if (verb) printf(">> %s", string); if (!strncmp(string, "PING", 4)) { @@ -156,7 +459,7 @@ raw_parser(char *string) { if (string[0] != ':') return; - if (olog) log_append(string, olog); + if (olog) logAppend(string, olog); char *tok, *prefix = strtok(string, " ") + 1, *suffix = strtok(NULL, ":"), *message = strtok(NULL, "\r"), *nickname = strtok(prefix, "!"), @@ -176,23 +479,27 @@ raw_parser(char *string) { printf("%*s--> \x1b[32;1m%s\x1b[0m\n", g - 3, "", nickname); return; } else if (!strncmp(command, "NICK", 4)) { - printf("\x1b[35;1m%*s\x1b[0m --> \x1b[35;1m%s\x1b[0m\n", g - 4, nickname, message); + printf("\x1b[35;1m%*s\x1b[0m ", g - 4, nickname); + printf("--> \x1b[35;1m%s\x1b[0m\n", message); return; - } else if (!strncmp(command, "PRIVMSG", 7) && strcmp(channel, nick) == 0) { - printf("%*s\x1b[43;1m%-.*s\x1b[0m ", s, "", g, nickname); - } else if (!strncmp(command, "PRIVMSG", 7) && strstr(channel, chan_default) == NULL) { - printf("%*s\x1b[33;1m%-.*s\x1b[0m [\x1b[33m%s\x1b[0m] ", s, "", \ - g, nickname, channel); - offset += 12 + strlen(channel); - } else printf("%*s\x1b[33;1m%-.*s\x1b[0m ", s, "", g, nickname); - message_wrap((message ? message : " "), offset); + } else if (!strncmp(command, "PRIVMSG", 7)) { + if (strcmp(channel, nick) == 0) { + printf("%*s\x1b[33;1m%-.*s\x1b[36m ", s, "", g, nickname); + } else if (strstr(channel, chan_default) == 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); + } else { + printf("%*s\x1b[33;1m%-.*s\x1b[0m ", s, "", g, nickname); + } + messageWrap((message ? message : " "), offset); } static char message_buffer[MSG_MAX + 1]; static size_t message_end = 0; -static int -handle_server_message(void) { +static int handleServerMessage(void) { for (;;) { ssize_t sl = read(conn, &message_buffer[message_end], MSG_MAX - message_end); if (sl == -1) { @@ -215,7 +522,7 @@ handle_server_message(void) { if (i != 0 && message_buffer[i - 1] == '\r' && message_buffer[i] == '\n') { char saved_char = message_buffer[i + 1]; message_buffer[i + 1] = '\0'; - raw_parser(message_buffer); + rawParser(message_buffer); message_buffer[i + 1] = saved_char; memmove(&message_buffer, &message_buffer[i + 1], message_end - i - 1); message_end = message_end - i - 1; @@ -228,63 +535,39 @@ handle_server_message(void) { } } -static void -handle_user_input(void) { - - char usrin[MSG_MAX], *tok; - - if (fgets(usrin, MSG_MAX, stdin) == NULL) { - perror("fgets"); - exit(EXIT_FAILURE); - } - +static void handleUserInput(char *usrin) { + char *tok; size_t msg_len = strlen(usrin); + if (usrin[msg_len - 1] == '\n') { usrin[msg_len - 1] = '\0'; } if (usrin[0] == '/' && usrin[1] == '#') { strcpy(chan_default, usrin + 2); - printf("new channel: #%s\n", chan_default); - } else if (usrin[0] == '/' && usrin[1] == '?' && msg_len == 3) { - printf("current channel: #%s\n", chan_default); + printf("\x1b[35mnew channel: #%s\x1b[0m\x1b[0F\n\n", chan_default); + } else if (usrin[0] == '/' && usrin[1] == '?') { + printf("\x1b[35mcurrent channel: #%s\x1b[0m\x1b[0F\n\n", chan_default); } else if (usrin[0] == '/') { raw("%s\r\n", usrin + 1); } else if (usrin[0] == '@') { strtok_r(usrin, " ", &tok); raw("privmsg %s :%s\r\n", usrin + 1, tok); + printf("\x1b[35mprivmsg %s :%s\x1b[0m\x1b[0F\n\n", usrin + 1, tok); } else { raw("privmsg #%s :%s\r\n", chan_default, usrin); + printf("\x1b[35mprivmsg #%s :%s\x1b[0m\x1b[0F\n\n", chan_default, usrin); } } -static int -keyboard_hit() { - - struct termios save, tp; - int byteswaiting; - - tcgetattr(STDIN_FILENO, &tp); - save = tp; - tp.c_lflag &= ~ICANON; - tcsetattr(STDIN_FILENO, TCSANOW, &tp); - ioctl(STDIN_FILENO, FIONREAD, &byteswaiting); - tcsetattr(STDIN_FILENO, TCSANOW, &save); - - return byteswaiting; -} - -static void -usage(void) { +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 columns] [-o path] \ +[-u username] [-k password] [-a token] [-x command] [-w nickwidth] [-o path] \ [-e|v|V]\n", stderr); exit(EXIT_FAILURE); } -int -main(int argc, char **argv) { - +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) { @@ -312,7 +595,7 @@ main(int argc, char **argv) { usage(); } - if (connection_initialize() != 0) { + if (connectionInit() != 0) { return EXIT_FAILURE; } @@ -332,18 +615,27 @@ main(int argc, char **argv) { fds[0].events = POLLIN; fds[1].events = POLLIN; + char usrin[MSG_MAX]; + int count, byteswaiting = 1; + for (;;) { int poll_res = poll(fds, 2, -1); if (poll_res != -1) { if (fds[0].revents & POLLIN) { - handle_user_input(); + byteswaiting = 0; + edit(usrin, MSG_MAX); + printf("\n\x1b[0F\x1b[0K"); + handleUserInput(usrin); + byteswaiting = 1; } - if (fds[1].revents & POLLIN && (keyboard_hit() < 1)) { - int rc = handle_server_message(); + if (fds[1].revents & POLLIN && byteswaiting) { + disableRawMode(); + int rc = handleServerMessage(); if (rc != 0) { if (rc == -2) return EXIT_FAILURE; return EXIT_SUCCESS; }; + if (enableRawMode(STDIN_FILENO) == -1) return -1; } } else { if (errno == EAGAIN) continue; |