├── .gitignore ├── Makefile ├── README.md ├── config.h ├── irc.c └── license /.gitignore: -------------------------------------------------------------------------------- 1 | irc 2 | *.sw[po] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN = irc 2 | 3 | CFLAGS = -std=c99 -Os -D_POSIX_C_SOURCE=201112 -D_GNU_SOURCE -D_XOPEN_CURSES -D_XOPEN_SOURCE_EXTENDED=1 -D_DEFAULT_SOURCE -D_BSD_SOURCE 4 | LDFLAGS = -lncursesw -lssl -lcrypto 5 | 6 | all: ${BIN} 7 | 8 | install: 9 | install -Dm755 ${BIN} $(DESTDIR)$(PREFIX)/bin/${BIN} 10 | 11 | uninstall: 12 | rm -f $(DESTDIR)$(PREFIX)/bin/${BIN} 13 | 14 | clean: 15 | rm -f ${BIN} *.o 16 | 17 | .PHONY: all clean 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # icyrc 2 | > no bs irc client 3 | 4 | ![scrot](https://x.icyphox.sh/ACE.png) 5 | 6 | Built on top of https://c9x.me/irc/. 7 | 8 | ## Installing 9 | 10 | Requires `ncurses` development files. 11 | Clone this repo and: 12 | 13 | ``` 14 | $ make 15 | # make install 16 | ``` 17 | 18 | Similarly, to uninstall: 19 | 20 | ``` 21 | # make uninstall 22 | ``` 23 | 24 | ## Usage 25 | 26 | ``` 27 | usage: irc [-n NICK] [-u USER] [-s SERVER] [-p PORT] [-l LOGFILE ] [-t] [-h] 28 | ``` 29 | 30 | The nick, user and password can be specified using `IRCNICK`, 31 | `USER` and `IRCPASS` environment variables. 32 | 33 | ### Commands 34 | 35 | - `/j #channel` — Join channel 36 | - `/l #channel` — Leave channel 37 | - `/me msg` — ACTION 38 | - `/q user msg` — Send private message 39 | - `/r something` — Send raw command 40 | - `/x` — Quit 41 | 42 | ### Hotkeys 43 | 44 | - Ctrl+n/p to cycle through buffers. 45 | - Emacs-like line editing commands: Ctrl+w/e/a etc. 46 | - PgUp and PgDn to scroll. 47 | 48 | ## Configuration 49 | 50 | In true suckless fashion, configuration is done via a `config.h` file. 51 | Recompile for changes to take effect. 52 | -------------------------------------------------------------------------------- /config.h: -------------------------------------------------------------------------------- 1 | #define VERSION "icyrc 0.2 (https://github.com/icyphox/icyrc)" 2 | 3 | #define SCROLL 15 4 | #define INDENT 25 5 | 6 | /* uncomment to enable date formatting; prepends to each msg */ 7 | // #define DATEFMT "%H:%M" 8 | 9 | /* normal msg "nick msg" */ 10 | #define PFMT "%-15s %s" 11 | /* action msg "nick msg" */ 12 | #define AFMT "* %-15s %s" 13 | /* highlight msg "nick msg" */ 14 | #define PFMTHIGH "%-15s] %s" 15 | 16 | /* command that STDOUTs a password in a single line */ 17 | #define PWCMD "pw -s ircpass" 18 | 19 | /* enable notifications (notify-send) */ 20 | 21 | #define NOTIFY 1 22 | 23 | /* server */ 24 | #define SRV "irc.icyphox.sh" 25 | /* port */ 26 | #define PORT "6666" 27 | 28 | -------------------------------------------------------------------------------- /irc.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #undef CTRL 26 | #define CTRL(x) (x & 037) 27 | 28 | #include "config.h" 29 | 30 | enum { 31 | ChanLen = 64, 32 | LineLen = 512, 33 | MaxChans = 16, 34 | BufSz = 2048, 35 | LogSz = 4096, 36 | MaxRecons = 10, /* -1 for infinitely many */ 37 | UtfSz = 4, 38 | RuneInvalid = 0xFFFD, 39 | }; 40 | 41 | typedef wchar_t Rune; 42 | 43 | static struct { 44 | int x; 45 | int y; 46 | WINDOW *sw, *mw, *iw; 47 | } scr; 48 | 49 | static struct Chan { 50 | char name[ChanLen]; 51 | char *buf, *eol; 52 | int n; /* Scroll offset. */ 53 | size_t sz; /* Size of buf. */ 54 | char high; /* Nick highlight. */ 55 | char new; /* New message. */ 56 | char join; /* Channel was 'j'-oined. */ 57 | } chl[MaxChans]; 58 | 59 | static int ssl; 60 | static struct { 61 | int fd; 62 | SSL *ssl; 63 | SSL_CTX *ctx; 64 | } srv; 65 | static char nick[64]; 66 | static int quit, winchg; 67 | static int nch, ch; /* Current number of channels, and current channel. */ 68 | static char outb[BufSz], *outp = outb; /* Output buffer. */ 69 | static FILE *logfp; 70 | 71 | static unsigned char utfbyte[UtfSz + 1] = {0x80, 0, 0xC0, 0xE0, 0xF0}; 72 | static unsigned char utfmask[UtfSz + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; 73 | static Rune utfmin[UtfSz + 1] = { 0, 0, 0x80, 0x800, 0x10000}; 74 | static Rune utfmax[UtfSz + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF}; 75 | 76 | static void scmd(char *, char *, char *, char *); 77 | static void tdrawbar(void); 78 | static void tredraw(void); 79 | static void treset(void); 80 | 81 | static void 82 | panic(const char *m) 83 | { 84 | treset(); 85 | fprintf(stderr, "error: %s\n", m); 86 | exit(1); 87 | } 88 | 89 | static size_t 90 | utf8validate(Rune *u, size_t i) 91 | { 92 | if (*u < utfmin[i] || *u > utfmax[i] || (0xD800 <= *u && *u <= 0xDFFF)) 93 | *u = RuneInvalid; 94 | for (i = 1; *u > utfmax[i]; ++i) 95 | ; 96 | return i; 97 | } 98 | 99 | static Rune 100 | utf8decodebyte(unsigned char c, size_t *i) 101 | { 102 | for (*i = 0; *i < UtfSz + 1; ++(*i)) 103 | if ((c & utfmask[*i]) == utfbyte[*i]) 104 | return c & ~utfmask[*i]; 105 | return 0; 106 | } 107 | 108 | static size_t 109 | utf8decode(char *c, Rune *u, size_t clen) 110 | { 111 | size_t i, j, len, type; 112 | Rune udecoded; 113 | 114 | *u = RuneInvalid; 115 | if (!clen) 116 | return 0; 117 | udecoded = utf8decodebyte(c[0], &len); 118 | if (len < 1 || len > UtfSz) 119 | return 1; 120 | for (i = 1, j = 1; i < clen && j < len; ++i, ++j) { 121 | udecoded = (udecoded << 6) | utf8decodebyte(c[i], &type); 122 | if (type != 0) 123 | return j; 124 | } 125 | if (j < len) 126 | return 0; 127 | *u = udecoded; 128 | utf8validate(u, len); 129 | return len; 130 | } 131 | 132 | static char 133 | utf8encodebyte(Rune u, size_t i) 134 | { 135 | return utfbyte[i] | (u & ~utfmask[i]); 136 | } 137 | 138 | static size_t 139 | utf8encode(Rune u, char *c) 140 | { 141 | size_t len, i; 142 | 143 | len = utf8validate(&u, 0); 144 | if (len > UtfSz) 145 | return 0; 146 | for (i = len - 1; i != 0; --i) { 147 | c[i] = utf8encodebyte(u, 0); 148 | u >>= 6; 149 | } 150 | c[0] = utf8encodebyte(u, len); 151 | return len; 152 | } 153 | 154 | static void 155 | sndf(const char *fmt, ...) 156 | { 157 | va_list vl; 158 | size_t n, l = BufSz - (outp - outb); 159 | 160 | if (l < 2) 161 | return; 162 | va_start(vl, fmt); 163 | n = vsnprintf(outp, l - 2, fmt, vl); 164 | va_end(vl); 165 | outp += n > l - 2 ? l - 2 : n; 166 | *outp++ = '\r'; 167 | *outp++ = '\n'; 168 | } 169 | 170 | static int 171 | srd(void) 172 | { 173 | static char l[BufSz], *p = l; 174 | char *s, *usr, *cmd, *par, *data; 175 | int rd; 176 | if (p - l >= BufSz) 177 | p = l; /* Input buffer overflow, there should something better to do. */ 178 | if (ssl) 179 | rd = SSL_read(srv.ssl, p, BufSz - (p - l)); 180 | else 181 | rd = read(srv.fd, p, BufSz - (p - l)); 182 | if (rd <= 0) 183 | return 0; 184 | p += rd; 185 | for (;;) { /* Cycle on all received lines. */ 186 | if (!(s = memchr(l, '\n', p - l))) 187 | return 1; 188 | if (s > l && s[-1] == '\r') 189 | s[-1] = 0; 190 | *s++ = 0; 191 | if (*l == ':') { 192 | if (!(cmd = strchr(l, ' '))) 193 | goto lskip; 194 | *cmd++ = 0; 195 | usr = l + 1; 196 | } else { 197 | usr = 0; 198 | cmd = l; 199 | } 200 | if (!(par = strchr(cmd, ' '))) 201 | goto lskip; 202 | *par++ = 0; 203 | if ((data = strchr(par, ':'))) 204 | *data++ = 0; 205 | scmd(usr, cmd, par, data); 206 | lskip: 207 | memmove(l, s, p - s); 208 | p -= s - l; 209 | } 210 | } 211 | 212 | static void 213 | sinit(const char *key, const char *nick, const char *user) 214 | { 215 | if (key) 216 | sndf("PASS %s", key); 217 | sndf("NICK %s", nick); 218 | sndf("USER %s 8 * :%s", user, user); 219 | sndf("MODE %s +i", nick); 220 | } 221 | 222 | static char * 223 | dial(const char *host, const char *service) 224 | { 225 | struct addrinfo hints, *res = NULL, *rp; 226 | int fd = -1, e; 227 | 228 | memset(&hints, 0, sizeof(hints)); 229 | hints.ai_family = AF_UNSPEC; /* allow IPv4 or IPv6 */ 230 | hints.ai_flags = AI_NUMERICSERV; /* avoid name lookup for port */ 231 | hints.ai_socktype = SOCK_STREAM; 232 | if ((e = getaddrinfo(host, service, &hints, &res))) 233 | return "Getaddrinfo failed."; 234 | for (rp = res; rp; rp = rp->ai_next) { 235 | if ((fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) == -1) 236 | continue; 237 | if (connect(fd, res->ai_addr, res->ai_addrlen) == -1) { 238 | close(fd); 239 | continue; 240 | } 241 | break; 242 | } 243 | if (fd == -1) 244 | return "Cannot connect to host."; 245 | srv.fd = fd; 246 | if (ssl) { 247 | SSL_load_error_strings(); 248 | SSL_library_init(); 249 | srv.ctx = SSL_CTX_new(SSLv23_client_method()); 250 | if (!srv.ctx) 251 | return "Could not initialize ssl context."; 252 | srv.ssl = SSL_new(srv.ctx); 253 | if (SSL_set_fd(srv.ssl, srv.fd) == 0 254 | || SSL_connect(srv.ssl) != 1) 255 | return "Could not connect with ssl."; 256 | } 257 | freeaddrinfo(res); 258 | return 0; 259 | } 260 | 261 | static void 262 | hangup(void) 263 | { 264 | if (srv.ssl) { 265 | SSL_shutdown(srv.ssl); 266 | SSL_free(srv.ssl); 267 | srv.ssl = 0; 268 | } 269 | if (srv.fd) { 270 | close(srv.fd); 271 | srv.fd = 0; 272 | } 273 | if (srv.ctx) { 274 | SSL_CTX_free(srv.ctx); 275 | srv.ctx = 0; 276 | } 277 | } 278 | 279 | static inline int 280 | chfind(const char *name) 281 | { 282 | int i; 283 | 284 | assert(name); 285 | for (i = nch - 1; i > 0; i--) 286 | if (!strcmp(chl[i].name, name)) 287 | break; 288 | return i; 289 | } 290 | 291 | static int 292 | chadd(const char *name, int joined) 293 | { 294 | int n; 295 | 296 | if (nch >= MaxChans || strlen(name) >= ChanLen) 297 | return -1; 298 | if ((n = chfind(name)) > 0) 299 | return n; 300 | strcpy(chl[nch].name, name); 301 | chl[nch].sz = LogSz; 302 | chl[nch].buf = malloc(LogSz); 303 | if (!chl[nch].buf) 304 | panic("out of memory"); 305 | chl[nch].eol = chl[nch].buf; 306 | chl[nch].n = 0; 307 | chl[nch].join = joined; 308 | if (joined) 309 | ch = nch; 310 | nch++; 311 | tdrawbar(); 312 | return nch; 313 | } 314 | 315 | static int 316 | chdel(char *name) 317 | { 318 | int n; 319 | 320 | if (!(n = chfind(name))) 321 | return 0; 322 | nch--; 323 | free(chl[n].buf); 324 | memmove(&chl[n], &chl[n + 1], (nch - n) * sizeof(struct Chan)); 325 | ch = nch - 1; 326 | tdrawbar(); 327 | return 1; 328 | } 329 | 330 | static char * 331 | pushl(char *p, char *e) 332 | { 333 | int x, cl; 334 | char *w; 335 | Rune u[2]; 336 | cchar_t cc; 337 | 338 | u[1] = 0; 339 | if ((w = memchr(p, '\n', e - p))) 340 | e = w + 1; 341 | w = p; 342 | x = 0; 343 | for (;;) { 344 | if (x >= scr.x) { 345 | waddch(scr.mw, '\n'); 346 | for (x = 0; x < INDENT; x++) 347 | waddch(scr.mw, ' '); 348 | if (*w == ' ') 349 | w++; 350 | x += p - w; 351 | } 352 | if (p >= e || *p == ' ' || p - w + INDENT >= scr.x - 1) { 353 | while (w < p) { 354 | w += utf8decode(w, u, UtfSz); 355 | if (wcwidth(*u) > 0 || *u == '\n') { 356 | setcchar(&cc, u, 0, 0, 0); 357 | wadd_wch(scr.mw, &cc); 358 | } 359 | } 360 | if (p >= e) 361 | return e; 362 | } 363 | p += utf8decode(p, u, UtfSz); 364 | if ((cl = wcwidth(*u)) >= 0) 365 | x += cl; 366 | } 367 | } 368 | 369 | static void 370 | pushf(int cn, const char *fmt, ...) 371 | { 372 | struct Chan *const c = &chl[cn]; 373 | size_t n, blen = c->eol - c->buf; 374 | va_list vl; 375 | time_t t; 376 | char *s; 377 | struct tm *tm, *gmtm; 378 | 379 | if (blen + LineLen >= c->sz) { 380 | c->sz *= 2; 381 | c->buf = realloc(c->buf, c->sz); 382 | if (!c->buf) 383 | panic("out of memory"); 384 | c->eol = c->buf + blen; 385 | } 386 | t = time(0); 387 | if (!(tm = localtime(&t))) 388 | panic("localtime failed"); 389 | #ifdef DATEFMT 390 | n = strftime(c->eol, LineLen, DATEFMT, tm); 391 | #endif 392 | if (!(gmtm = gmtime(&t))) 393 | panic("gmtime failed"); 394 | c->eol[n++] = ' '; 395 | va_start(vl, fmt); 396 | s = c->eol + n; 397 | n += vsnprintf(s, LineLen - n - 1, fmt, vl); 398 | va_end(vl); 399 | 400 | if (logfp) { 401 | fprintf(logfp, "%-12.12s\t%04d-%02d-%02dT%02d:%02d:%02dZ\t%s\n", 402 | c->name, 403 | gmtm->tm_year + 1900, gmtm->tm_mon + 1, gmtm->tm_mday, 404 | gmtm->tm_hour, gmtm->tm_min, gmtm->tm_sec, s); 405 | fflush(logfp); 406 | } 407 | 408 | strcat(c->eol, "\n"); 409 | if (n >= LineLen - 1) 410 | c->eol += LineLen - 1; 411 | else 412 | c->eol += n + 1; 413 | if (cn == ch && c->n == 0) { 414 | char *p = c->eol - n - 1; 415 | 416 | if (p != c->buf) 417 | waddch(scr.mw, '\n'); 418 | pushl(p, c->eol - 1); 419 | wrefresh(scr.mw); 420 | } 421 | } 422 | 423 | char 424 | *strremove(char *str, const char *sub) { 425 | size_t len = strlen(sub); 426 | if (len > 0) { 427 | char *p = str; 428 | size_t size = 0; 429 | while ((p = strstr(p, sub)) != NULL) { 430 | size = (size == 0) ? (p - str) + strlen(p + len) + 1 : size - len; 431 | memmove(p, p + len, size - (p - str)); 432 | } 433 | } 434 | return str; 435 | } 436 | 437 | static void 438 | scmd(char *usr, char *cmd, char *par, char *data) 439 | { 440 | int s, c; 441 | int pushed = 0; 442 | char *pm = strtok(par, " "), *chan; 443 | if (!usr) 444 | usr = "?"; 445 | else { 446 | char *bang = strchr(usr, '!'); 447 | if (bang) 448 | *bang = 0; 449 | } 450 | if (!strcmp(cmd, "PRIVMSG")) { 451 | if (!strcmp(data, "\001VERSION\001")) 452 | sndf("NOTICE %s :\001VERSION %s\001", usr, VERSION); 453 | if (strstr(data, "\001PING") != NULL) 454 | sndf("NOTICE %s :%s", usr, data); 455 | if (!pm || !data) 456 | return; 457 | if (strchr("&#!+.~", pm[0])) 458 | chan = pm; 459 | else 460 | chan = usr; 461 | if (!(c = chfind(chan))) { 462 | if (chadd(chan, 0) < 0) 463 | return; 464 | tredraw(); 465 | } 466 | c = chfind(chan); 467 | if (strstr(data, "\001ACTION") != NULL) { 468 | char *s = strremove(data, "\001ACTION "); 469 | pushf(c, AFMT, usr, s); 470 | pushed = 1; 471 | } 472 | if (strcasestr(data, nick)) { 473 | pushf(c, PFMTHIGH, usr, data); 474 | pushed = 1; 475 | char cmd[256]; 476 | if (NOTIFY) { 477 | /* packs all of notify-send into cmd */ 478 | snprintf(cmd, sizeof(cmd), "notify-send \"%s @ %s\" \"%s\"", usr, chan, data); 479 | system(cmd); 480 | } 481 | chl[c].high |= ch != c; 482 | } 483 | if (!pushed) { 484 | pushf(c, PFMT, usr, data); 485 | } 486 | if (ch != c) { 487 | chl[c].new = 1; 488 | tdrawbar(); 489 | } 490 | } else if (!strcmp(cmd, "PING")) { 491 | sndf("PONG :%s", data ? data : "(null)"); 492 | } else if (!strcmp(cmd, "PART")) { 493 | if (!pm) 494 | return; 495 | pushf(chfind(pm), "! %-12s has left %s", usr, pm); 496 | } else if (!strcmp(cmd, "JOIN")) { 497 | if (!pm) 498 | return; 499 | pushf(chfind(pm), "! %-12s has joined %s", usr, pm); 500 | } else if (!strcmp(cmd, "470")) { /* Channel forwarding. */ 501 | char *ch = strtok(0, " "), *fch = strtok(0, " "); 502 | 503 | if (!ch || !fch || !(s = chfind(ch))) 504 | return; 505 | chl[s].name[0] = 0; 506 | strncat(chl[s].name, fch, ChanLen - 1); 507 | tdrawbar(); 508 | } else if (!strcmp(cmd, "471") || !strcmp(cmd, "473") 509 | || !strcmp(cmd, "474") || !strcmp(cmd, "475")) { /* Join error. */ 510 | if ((pm = strtok(0, " "))) { 511 | chdel(pm); 512 | pushf(0, "-!- Cannot join channel %s (%s)", pm, cmd); 513 | tredraw(); 514 | } 515 | } else if (!strcmp(cmd, "QUIT")) { /* Commands we don't care about. */ 516 | return; 517 | } else if (!strcmp(cmd, "NOTICE") || !strcmp(cmd, "375") 518 | || !strcmp(cmd, "372") || !strcmp(cmd, "376")) { 519 | pushf(0, "%s", data ? data : ""); 520 | } else 521 | pushf(0, "%s - %s %s", cmd, par, data ? data : "(null)"); 522 | } 523 | 524 | static void 525 | uparse(char *m) 526 | { 527 | char *p = m; 528 | //if (!p[0]|| (p[1] != ' ' && p[1] != 0)) { 529 | if (!strncmp("/j", p, 2)) { /* Join channels. */ 530 | p += 1 + (p[2] == ' '); 531 | p = strtok(p, " "); 532 | while (p) { 533 | if (chadd(p, 1) < 0) 534 | break; 535 | sndf("JOIN %s", p); 536 | p = strtok(0, " "); 537 | } 538 | tredraw(); 539 | return; 540 | } 541 | if (!strncmp("/l", p, 2)) {/* Leave channels. */ 542 | p += 1 + (p[2] == ' '); 543 | if (!*p) { 544 | if (ch == 0) 545 | return; /* Cannot leave server window. */ 546 | strcat(p, chl[ch].name); 547 | } 548 | p = strtok(p, " "); 549 | while (p) { 550 | if (chdel(p)) 551 | sndf("PART %s", p); 552 | p = strtok(0, " "); 553 | } 554 | tredraw(); 555 | return; 556 | } 557 | if (!strncmp("/q", p, 2)) { /* Private message. */ 558 | int len = strlen(m); 559 | char *temp = (char *) malloc (len * (sizeof(char))), *msg; 560 | strcpy(temp, p); 561 | temp += 3; 562 | char *u = strtok(temp, " "); 563 | msg = strtok(NULL, "\0"); 564 | chadd(u, 1); 565 | pushf(chfind(u), PFMT, nick, msg); 566 | sndf("PRIVMSG %s :%s", u, msg); 567 | tredraw(); 568 | return; 569 | } 570 | if (!strncmp("/r", p, 2)) { /* Send raw. */ 571 | if (p[1]) 572 | sndf("%s", &p[3]); 573 | return; 574 | } 575 | if (!strncmp("/x", p, 2)) {/* Quit. */ 576 | quit = 1; 577 | return; 578 | } 579 | if (!strncmp("/me", p, 3)) { 580 | char *s = strremove(p, "/me"); 581 | pushf(ch, AFMT, nick, s); 582 | sndf("PRIVMSG %s :\001ACTION %s\001", chl[ch].name, s); 583 | } 584 | else { 585 | if (ch == 0) 586 | return; 587 | m += strspn(m, " "); 588 | if (!*m) 589 | return; 590 | pushf(ch, PFMT, nick, m); 591 | sndf("PRIVMSG %s :%s", chl[ch].name, m); 592 | return; 593 | }/* Send on current channel. */ 594 | } 595 | 596 | static void 597 | sigwinch(int sig) 598 | { 599 | if (sig) 600 | winchg = 1; 601 | } 602 | 603 | static void 604 | tinit(void) 605 | { 606 | setlocale(LC_ALL, ""); 607 | signal(SIGWINCH, sigwinch); 608 | initscr(); 609 | raw(); 610 | noecho(); 611 | getmaxyx(stdscr, scr.y, scr.x); 612 | if (scr.y < 4) 613 | panic("screen too small"); 614 | if ((scr.sw = newwin(1, scr.x, 0, 0)) == 0 615 | || (scr.mw = newwin(scr.y - 2, scr.x, 1, 0)) == 0 616 | || (scr.iw = newwin(1, scr.x, scr.y - 1, 0)) == 0) 617 | panic("cannot create windows"); 618 | keypad(scr.iw, 1); 619 | scrollok(scr.mw, 1); 620 | if (has_colors() == TRUE) { 621 | start_color(); 622 | use_default_colors(); 623 | init_pair(1, COLOR_BLACK, COLOR_WHITE); 624 | init_pair(2, COLOR_RED, COLOR_WHITE); 625 | init_pair(3, COLOR_GREEN, COLOR_WHITE); 626 | wbkgd(scr.sw, COLOR_PAIR(1)); 627 | } 628 | } 629 | 630 | static void 631 | tresize(void) 632 | { 633 | struct winsize ws; 634 | 635 | winchg = 0; 636 | if (ioctl(0, TIOCGWINSZ, &ws) < 0) 637 | panic("ioctl (TIOCGWINSZ) failed"); 638 | if (ws.ws_row <= 2) 639 | return; 640 | resizeterm(scr.y = ws.ws_row, scr.x = ws.ws_col); 641 | wresize(scr.mw, scr.y - 2, scr.x); 642 | wresize(scr.iw, 1, scr.x); 643 | wresize(scr.sw, 1, scr.x); 644 | mvwin(scr.iw, scr.y - 1, 0); 645 | tredraw(); 646 | tdrawbar(); 647 | } 648 | 649 | static void 650 | tredraw(void) 651 | { 652 | struct Chan *const c = &chl[ch]; 653 | char *q, *p; 654 | int nl = -1; 655 | 656 | if (c->eol == c->buf) { 657 | wclear(scr.mw); 658 | wrefresh(scr.mw); 659 | return; 660 | } 661 | p = c->eol - 1; 662 | if (c->n) { 663 | int i = c->n; 664 | for (; p > c->buf; p--) 665 | if (*p == '\n' && !i--) 666 | break; 667 | if (p == c->buf) 668 | c->n -= i; 669 | } 670 | q = p; 671 | while (nl < scr.y - 2) { 672 | while (*q != '\n' && q > c->buf) 673 | q--; 674 | nl++; 675 | if (q == c->buf) 676 | break; 677 | q--; 678 | } 679 | if (q != c->buf) 680 | q += 2; 681 | wclear(scr.mw); 682 | wmove(scr.mw, 0, 0); 683 | while (q < p) 684 | q = pushl(q, p); 685 | wrefresh(scr.mw); 686 | } 687 | 688 | static void 689 | tdrawbar(void) 690 | { 691 | size_t l; 692 | int fst = ch; 693 | 694 | for (l = 0; fst > 0 && l < scr.x / 2; fst--) 695 | l += strlen(chl[fst].name) + 3; 696 | 697 | werase(scr.sw); 698 | for (l = 0; fst < nch && l < scr.x; fst++) { 699 | char *p = chl[fst].name; 700 | if (fst == ch) 701 | wattron(scr.sw, A_REVERSE); 702 | waddstr(scr.sw, " "), l++; 703 | if (chl[fst].high) { 704 | wattron(scr.sw, COLOR_PAIR(2)), l++; 705 | } 706 | else if (chl[fst].new) 707 | wattron(scr.sw, COLOR_PAIR(3)), l++; 708 | for (; *p && l < scr.x; p++, l++) 709 | waddch(scr.sw, *p); 710 | if (l < scr.x - 1) 711 | waddstr(scr.sw, " "), l += 2; 712 | if (fst == ch) 713 | wattroff(scr.sw, A_REVERSE); 714 | wattroff(scr.sw, COLOR_PAIR(2)); 715 | wattroff(scr.sw, COLOR_PAIR(3)); 716 | } 717 | wrefresh(scr.sw); 718 | } 719 | 720 | static void 721 | tgetch(void) 722 | { 723 | static char l[BufSz]; 724 | static size_t shft, cu, len; 725 | size_t dirty = len + 1, i; 726 | int c; 727 | 728 | c = wgetch(scr.iw); 729 | switch (c) { 730 | case CTRL('n'): 731 | ch = (ch + 1) % nch; 732 | chl[ch].high = chl[ch].new = 0; 733 | tdrawbar(); 734 | tredraw(); 735 | return; 736 | case CTRL('p'): 737 | ch = (ch + nch - 1) % nch; 738 | chl[ch].high = chl[ch].new = 0; 739 | tdrawbar(); 740 | tredraw(); 741 | return; 742 | case KEY_PPAGE: 743 | chl[ch].n += SCROLL; 744 | tredraw(); 745 | return; 746 | case KEY_NPAGE: 747 | chl[ch].n -= SCROLL; 748 | if (chl[ch].n < 0) 749 | chl[ch].n = 0; 750 | tredraw(); 751 | return; 752 | case CTRL('a'): 753 | case KEY_HOME: 754 | cu = 0; 755 | break; 756 | case CTRL('e'): 757 | case KEY_END: 758 | cu = len; 759 | break; 760 | case CTRL('b'): 761 | case KEY_LEFT: 762 | if (cu) 763 | cu--; 764 | break; 765 | case KEY_RIGHT: 766 | if (cu < len) 767 | cu++; 768 | break; 769 | case CTRL('k'): 770 | dirty = len = cu; 771 | break; 772 | case CTRL('u'): 773 | if (cu == 0) 774 | return; 775 | len -= cu; 776 | memmove(l, &l[cu], len); 777 | dirty = cu = 0; 778 | break; 779 | case CTRL('d'): 780 | if (cu >= len) 781 | return; 782 | memmove(&l[cu], &l[cu + 1], len - cu - 1); 783 | dirty = cu; 784 | len--; 785 | break; 786 | case CTRL('h'): 787 | case KEY_BACKSPACE: 788 | if (cu == 0) 789 | return; 790 | memmove(&l[cu - 1], &l[cu], len - cu); 791 | dirty = --cu; 792 | len--; 793 | break; 794 | case CTRL('w'): 795 | if (cu == 0) 796 | break; 797 | i = 1; 798 | while (l[cu - i] == ' ' && cu - i != 0) i++; 799 | while (l[cu - i] != ' ' && cu - i != 0) i++; 800 | if (cu - i != 0) i--; 801 | memmove(&l[cu - i], &l[cu], len - cu); 802 | cu -= i; 803 | dirty = cu; 804 | len -= i; 805 | break; 806 | case '\n': 807 | l[len] = 0; 808 | uparse(l); 809 | dirty = cu = len = 0; 810 | break; 811 | default: 812 | if (c > CHAR_MAX || len >= BufSz - 1) 813 | return; /* Skip other curses codes. */ 814 | memmove(&l[cu + 1], &l[cu], len - cu); 815 | dirty = cu; 816 | len++; 817 | l[cu++] = c; 818 | break; 819 | } 820 | while (cu < shft) 821 | dirty = 0, shft -= shft >= scr.x / 2 ? scr.x / 2 : shft; 822 | while (cu >= scr.x + shft) 823 | dirty = 0, shft += scr.x / 2; 824 | if (dirty <= shft) 825 | i = shft; 826 | else if (dirty > scr.x + shft || dirty > len) 827 | goto mvcur; 828 | else 829 | i = dirty; 830 | wmove(scr.iw, 0, i - shft); 831 | wclrtoeol(scr.iw); 832 | for (; i - shft < scr.x && i < len; i++) 833 | waddch(scr.iw, l[i]); 834 | mvcur: wmove(scr.iw, 0, cu - shft); 835 | } 836 | 837 | static void 838 | treset(void) 839 | { 840 | if (scr.mw) 841 | delwin(scr.mw); 842 | if (scr.sw) 843 | delwin(scr.sw); 844 | if (scr.iw) 845 | delwin(scr.iw); 846 | endwin(); 847 | } 848 | 849 | int 850 | main(int argc, char *argv[]) 851 | { 852 | const char *user = getenv("USER"); 853 | const char *ircnick = getenv("IRCNICK"); 854 | char key[128]; 855 | #ifdef PWCMD 856 | FILE *fp = popen(PWCMD, "r"); 857 | if (fp == NULL) { 858 | panic("failed to run command"); 859 | } 860 | fgets(key, 128, fp); 861 | key[strcspn(key, "\n")] = 0; 862 | pclose(fp); 863 | #else 864 | if(getenv("IRCPASS")) 865 | strcpy(key, getenv("IRCPASS")); 866 | else 867 | panic("error: IRCPASS environment variable not set"); 868 | #endif 869 | const char *server = SRV; 870 | const char *port = PORT; 871 | char *err; 872 | int o, reconn; 873 | 874 | signal(SIGPIPE, SIG_IGN); 875 | while ((o = getopt(argc, argv, "thk:n:u:s:p:l:")) >= 0) 876 | switch (o) { 877 | case 'h': 878 | case '?': 879 | usage: 880 | fputs("usage: irc [-n NICK] [-u USER] [-s SERVER] [-p PORT] [-l LOGFILE ] [-t] [-h]\n", stderr); 881 | exit(0); 882 | case 'l': 883 | if (!(logfp = fopen(optarg, "a"))) 884 | panic("fopen: logfile"); 885 | break; 886 | case 'n': 887 | if (strlen(optarg) >= sizeof nick) 888 | goto usage; 889 | strcpy(nick, optarg); 890 | break; 891 | case 't': 892 | ssl = 1; 893 | break; 894 | case 'u': 895 | user = optarg; 896 | break; 897 | case 's': 898 | server = optarg; 899 | break; 900 | case 'p': 901 | port = optarg; 902 | break; 903 | } 904 | if (!user) 905 | user = "anonymous"; 906 | if (!nick[0] && ircnick && strlen(ircnick) < sizeof nick) 907 | strcpy(nick, ircnick); 908 | if (!nick[0] && strlen(user) < sizeof nick) 909 | strcpy(nick, user); 910 | if (!nick[0]) 911 | goto usage; 912 | tinit(); 913 | err = dial(server, port); 914 | if (err) 915 | panic(err); 916 | chadd(server, 0); 917 | sinit(key, nick, user); 918 | reconn = 0; 919 | while (!quit) { 920 | struct timeval t = {.tv_sec = 5}; 921 | struct Chan *c; 922 | fd_set rfs, wfs; 923 | int ret; 924 | 925 | if (winchg) 926 | tresize(); 927 | FD_ZERO(&wfs); 928 | FD_ZERO(&rfs); 929 | FD_SET(0, &rfs); 930 | if (!reconn) { 931 | FD_SET(srv.fd, &rfs); 932 | if (outp != outb) 933 | FD_SET(srv.fd, &wfs); 934 | } 935 | ret = select(srv.fd + 1, &rfs, &wfs, 0, &t); 936 | if (ret < 0) { 937 | if (errno == EINTR) 938 | continue; 939 | panic("select failed"); 940 | } 941 | if (reconn) { 942 | hangup(); 943 | if (reconn++ == MaxRecons + 1) 944 | panic("link lost"); 945 | pushf(0, "-!- Link lost, attempting reconnection..."); 946 | if (dial(server, port) != 0) 947 | continue; 948 | sinit(key, nick, user); 949 | for (c = chl; c < &chl[nch]; ++c) 950 | if (c->join) 951 | sndf("JOIN %s", c->name); 952 | reconn = 0; 953 | } 954 | if (FD_ISSET(srv.fd, &rfs)) { 955 | if (!srd()) { 956 | reconn = 1; 957 | continue; 958 | } 959 | } 960 | if (FD_ISSET(srv.fd, &wfs)) { 961 | int wr; 962 | 963 | if (ssl) 964 | wr = SSL_write(srv.ssl, outb, outp - outb); 965 | else 966 | wr = write(srv.fd, outb, outp - outb); 967 | if (wr <= 0) { 968 | reconn = wr < 0; 969 | continue; 970 | } 971 | outp -= wr; 972 | memmove(outb, outb + wr, outp - outb); 973 | } 974 | if (FD_ISSET(0, &rfs)) { 975 | tgetch(); 976 | wrefresh(scr.iw); 977 | } 978 | } 979 | hangup(); 980 | while (nch--) 981 | free(chl[nch].buf); 982 | treset(); 983 | exit(0); 984 | } 985 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Anirudh Oppiliappan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | --------------------------------------------------------------------------------