/* Copyright (C) 2020 C. McEnroe * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * Additional permission under GNU GPL version 3 section 7: * * If you modify this Program, or any covered work, by linking or * combining it with OpenSSL (or a modified version of that library), * containing parts covered by the terms of the OpenSSL License and the * original SSLeay license, the licensors of this Program grant you * additional permission to convey the resulting work. Corresponding * Source for a non-source form of such a combination shall include the * source code for the parts of OpenSSL used as well as that of the * covered work. */ #define _XOPEN_SOURCE_EXTENDED #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __FreeBSD__ #include #endif #include "chat.h" #include "edit.h" // Annoying stuff from : #undef lines #undef tab #define BOTTOM (LINES - 1) #define RIGHT (COLS - 1) #define MAIN_LINES (LINES - StatusLines - InputLines) WINDOW *uiStatus; WINDOW *uiMain; static WINDOW *input; static short colorPairs; static void colorInit(void) { start_color(); use_default_colors(); if (!COLORS) return; for (short pair = 0; pair < 16; ++pair) { init_pair(1 + pair, pair % COLORS, -1); } colorPairs = 17; } static attr_t colorAttr(short fg) { if (!COLORS) return (fg > 0 ? A_BOLD : A_NORMAL); if (fg != COLOR_BLACK && fg % COLORS == COLOR_BLACK) return A_BOLD; if (COLORS > 8) return A_NORMAL; return (fg / COLORS & 1 ? A_BOLD : A_NORMAL); } static short colorPair(short fg, short bg) { if (!COLORS) return 0; fg %= COLORS; bg %= COLORS; if (bg == -1 && fg < 16) return 1 + fg; for (short pair = 17; pair < colorPairs; ++pair) { short f, b; pair_content(pair, &f, &b); if (f == fg && b == bg) return pair; } init_pair(colorPairs, fg, bg); return colorPairs++; } #define ENUM_KEY \ X(KeyCtrlLeft, "\33[1;5D", NULL) \ X(KeyCtrlRight, "\33[1;5C", NULL) \ X(KeyMeta0, "\0330", "\33)") \ X(KeyMeta1, "\0331", "\33!") \ X(KeyMeta2, "\0332", "\33@") \ X(KeyMeta3, "\0333", "\33#") \ X(KeyMeta4, "\0334", "\33$") \ X(KeyMeta5, "\0335", "\33%") \ X(KeyMeta6, "\0336", "\33^") \ X(KeyMeta7, "\0337", "\33&") \ X(KeyMeta8, "\0338", "\33*") \ X(KeyMeta9, "\0339", "\33(") \ X(KeyMetaA, "\33a", NULL) \ X(KeyMetaB, "\33b", NULL) \ X(KeyMetaD, "\33d", NULL) \ X(KeyMetaF, "\33f", NULL) \ X(KeyMetaL, "\33l", NULL) \ X(KeyMetaM, "\33m", NULL) \ X(KeyMetaN, "\33n", NULL) \ X(KeyMetaP, "\33p", NULL) \ X(KeyMetaQ, "\33q", NULL) \ X(KeyMetaS, "\33s", NULL) \ X(KeyMetaT, "\33t", NULL) \ X(KeyMetaU, "\33u", NULL) \ X(KeyMetaV, "\33v", NULL) \ X(KeyMetaEnter, "\33\r", "\33\n") \ X(KeyMetaGt, "\33>", "\33.") \ X(KeyMetaLt, "\33<", "\33,") \ X(KeyMetaEqual, "\33=", NULL) \ X(KeyMetaMinus, "\33-", "\33_") \ X(KeyMetaPlus, "\33+", NULL) \ X(KeyMetaSlash, "\33/", "\33?") \ X(KeyFocusIn, "\33[I", NULL) \ X(KeyFocusOut, "\33[O", NULL) \ X(KeyPasteOn, "\33[200~", NULL) \ X(KeyPasteOff, "\33[201~", NULL) \ X(KeyPasteManual, "\32p", "\32\20") enum { KeyMax = KEY_MAX, #define X(id, seq, alt) id, ENUM_KEY #undef X }; // XXX: Assuming terminals will be fine with these even if they're unsupported, // since they're "private" modes. static const char *FocusMode[2] = { "\33[?1004l", "\33[?1004h" }; static const char *PasteMode[2] = { "\33[?2004l", "\33[?2004h" }; static void errExit(void) { putp(FocusMode[false]); putp(PasteMode[false]); reset_shell_mode(); } void uiInitEarly(void) { initscr(); cbreak(); noecho(); colorInit(); atexit(errExit); #ifndef A_ITALIC #define A_ITALIC A_BLINK // Force ncurses to use individual enter_attr_mode strings: set_attributes = NULL; enter_blink_mode = enter_italics_mode; #endif if (!to_status_line && !strncmp(termname(), "xterm", 5)) { to_status_line = "\33]2;"; from_status_line = "\7"; } #define X(id, seq, alt) define_key(seq, id); if (alt) define_key(alt, id); ENUM_KEY #undef X uiStatus = newwin(StatusLines, COLS, 0, 0); if (!uiStatus) err(EX_OSERR, "newwin"); uiMain = newwin(MAIN_LINES, COLS, StatusLines, 0); if (!uiMain) err(EX_OSERR, "newwin"); input = newpad(InputLines, InputCols); if (!input) err(EX_OSERR, "newpad"); keypad(input, true); nodelay(input, true); windowInit(); uiShow(); } // Avoid disabling VINTR until main loop. void uiInitLate(void) { struct termios term; int error = tcgetattr(STDOUT_FILENO, &term); if (error) err(EX_OSERR, "tcgetattr"); // Gain use of C-q, C-s, C-c, C-z, C-y, C-v, C-o. term.c_iflag &= ~IXON; term.c_cc[VINTR] = _POSIX_VDISABLE; term.c_cc[VSUSP] = _POSIX_VDISABLE; #ifdef VDSUSP term.c_cc[VDSUSP] = _POSIX_VDISABLE; #endif term.c_cc[VLNEXT] = _POSIX_VDISABLE; term.c_cc[VDISCARD] = _POSIX_VDISABLE; error = tcsetattr(STDOUT_FILENO, TCSANOW, &term); if (error) err(EX_OSERR, "tcsetattr"); def_prog_mode(); } static bool hidden = true; static bool waiting; char uiTitle[TitleCap]; static char prevTitle[TitleCap]; void uiDraw(void) { if (hidden) return; wnoutrefresh(uiStatus); wnoutrefresh(uiMain); int y, x; getyx(input, y, x); pnoutrefresh( input, 0, (x + 1 > RIGHT ? x + 1 - RIGHT : 0), LINES - InputLines, 0, BOTTOM, RIGHT ); (void)y; doupdate(); if (!to_status_line) return; if (!strcmp(uiTitle, prevTitle)) return; strcpy(prevTitle, uiTitle); putp(to_status_line); putp(uiTitle); putp(from_status_line); fflush(stdout); } static const short Colors[ColorCap] = { [Default] = -1, [White] = 8 + COLOR_WHITE, [Black] = 0 + COLOR_BLACK, [Blue] = 0 + COLOR_BLUE, [Green] = 0 + COLOR_GREEN, [Red] = 8 + COLOR_RED, [Brown] = 0 + COLOR_RED, [Magenta] = 0 + COLOR_MAGENTA, [Orange] = 0 + COLOR_YELLOW, [Yellow] = 8 + COLOR_YELLOW, [LightGreen] = 8 + COLOR_GREEN, [Cyan] = 0 + COLOR_CYAN, [LightCyan] = 8 + COLOR_CYAN, [LightBlue] = 8 + COLOR_BLUE, [Pink] = 8 + COLOR_MAGENTA, [Gray] = 8 + COLOR_BLACK, [LightGray] = 0 + COLOR_WHITE, 52, 94, 100, 58, 22, 29, 23, 24, 17, 54, 53, 89, 88, 130, 142, 64, 28, 35, 30, 25, 18, 91, 90, 125, 124, 166, 184, 106, 34, 49, 37, 33, 19, 129, 127, 161, 196, 208, 226, 154, 46, 86, 51, 75, 21, 171, 201, 198, 203, 215, 227, 191, 83, 122, 87, 111, 63, 177, 207, 205, 217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212, 16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231, }; uint uiAttr(struct Style style) { attr_t attr = A_NORMAL; if (style.attr & Bold) attr |= A_BOLD; if (style.attr & Reverse) attr |= A_REVERSE; if (style.attr & Italic) attr |= A_ITALIC; if (style.attr & Underline) attr |= A_UNDERLINE; return attr | colorAttr(Colors[style.fg]); } static bool spoilerReveal; short uiPair(struct Style style) { if (spoilerReveal && style.fg == style.bg) { return colorPair(Colors[Default], Colors[style.bg]); } return colorPair(Colors[style.fg], Colors[style.bg]); } void uiShow(void) { if (!hidden) return; prevTitle[0] = '\0'; putp(FocusMode[true]); putp(PasteMode[true]); fflush(stdout); hidden = false; windowUnmark(); } void uiHide(void) { if (hidden) return; windowMark(); hidden = true; putp(FocusMode[false]); putp(PasteMode[false]); endwin(); } void uiWait(void) { waiting = true; } struct Util uiNotifyUtil; static void notify(uint id, const char *str) { if (self.restricted) return; if (!uiNotifyUtil.argc) return; char buf[1024]; styleStrip(buf, sizeof(buf), str); struct Util util = uiNotifyUtil; utilPush(&util, idNames[id]); utilPush(&util, buf); pid_t pid = fork(); if (pid < 0) err(EX_OSERR, "fork"); if (pid) return; setsid(); close(STDIN_FILENO); dup2(utilPipe[1], STDOUT_FILENO); dup2(utilPipe[1], STDERR_FILENO); execvp(util.argv[0], (char *const *)util.argv); warn("%s", util.argv[0]); _exit(EX_CONFIG); } void uiWrite(uint id, enum Heat heat, const time_t *src, const char *str) { bool note = windowWrite(id, heat, src, str); if (note) { beep(); notify(id, str); } } void uiFormat( uint id, enum Heat heat, const time_t *time, const char *format, ... ) { char buf[1024]; va_list ap; va_start(ap, format); int len = vsnprintf(buf, sizeof(buf), format, ap); va_end(ap); assert((size_t)len < sizeof(buf)); uiWrite(id, heat, time, buf); } static void resize(void) { wclear(uiMain); wresize(uiMain, MAIN_LINES, COLS); windowResize(); } static void inputAdd(struct Style reset, struct Style *style, const char *str) { while (*str) { const char *code = str; size_t len = styleParse(style, &str); wattr_set(input, A_BOLD | A_REVERSE, 0, NULL); switch (*code) { break; case B: waddch(input, 'B'); break; case C: waddch(input, 'C'); break; case O: waddch(input, 'O'); break; case R: waddch(input, 'R'); break; case I: waddch(input, 'I'); break; case U: waddch(input, 'U'); break; case '\n': waddch(input, 'N'); } if (str - code > 1) waddnstr(input, &code[1], str - &code[1]); if (str[0] == '\n') { *style = reset; str++; len--; } size_t nl = strcspn(str, "\n"); if (nl < len) len = nl; wattr_set(input, uiAttr(*style), uiPair(*style), NULL); waddnstr(input, str, len); str += len; } } static char *inputStop( struct Style reset, struct Style *style, const char *str, char *stop ) { char ch = *stop; *stop = '\0'; inputAdd(reset, style, str); *stop = ch; return stop; } static struct Edit edit; void uiUpdate(void) { char *buf = editString(&edit); uint id = windowID(); const char *prefix = ""; const char *prompt = self.nick; const char *suffix = ""; const char *skip = buf; struct Style stylePrompt = { .fg = self.color, .bg = Default }; struct Style styleInput = StyleDefault; size_t split = commandWillSplit(id, buf); const char *privmsg = commandIsPrivmsg(id, buf); const char *notice = commandIsNotice(id, buf); const char *action = commandIsAction(id, buf); if (privmsg) { prefix = "<"; suffix = "> "; skip = privmsg; } else if (notice) { prefix = "-"; suffix = "- "; styleInput.fg = LightGray; skip = notice; } else if (action) { prefix = "* "; suffix = " "; stylePrompt.attr |= Italic; styleInput.attr |= Italic; skip = action; } else if (id == Debug && buf[0] != '/') { prompt = "<< "; stylePrompt.fg = Gray; } else { prompt = ""; } if (skip > &buf[edit.mbs.pos]) { prefix = prompt = suffix = ""; skip = buf; } int y, x; wmove(input, 0, 0); if (windowTimeEnable() && id != Network) { whline(input, ' ', windowTime.width); wmove(input, 0, windowTime.width); } wattr_set(input, uiAttr(stylePrompt), uiPair(stylePrompt), NULL); waddstr(input, prefix); waddstr(input, prompt); waddstr(input, suffix); getyx(input, y, x); int pos; struct Style style = styleInput; inputStop(styleInput, &style, skip, &buf[edit.mbs.pos]); getyx(input, y, pos); wmove(input, y, x); style = styleInput; const char *ptr = skip; if (split) { ptr = inputStop(styleInput, &style, ptr, &buf[split]); style = styleInput; style.bg = Red; } inputAdd(styleInput, &style, ptr); wclrtoeol(input); wmove(input, y, pos); } static void inputEnter(void) { command(windowID(), editString(&edit)); editFn(&edit, EditClear); } static void keyCode(int code) { switch (code) { break; case KEY_RESIZE: resize(); break; case KeyFocusIn: windowUnmark(); break; case KeyFocusOut: windowMark(); break; case KeyMetaEnter: editInsert(&edit, L'\n'); break; case KeyMetaEqual: windowToggleMute(); break; case KeyMetaMinus: windowToggleThresh(-1); break; case KeyMetaPlus: windowToggleThresh(+1); break; case KeyMetaSlash: windowSwap(); break; case KeyMetaGt: windowScroll(ScrollAll, -1); break; case KeyMetaLt: windowScroll(ScrollAll, +1); break; case KeyMeta0 ... KeyMeta9: windowShow(code - KeyMeta0); break; case KeyMetaA: windowAuto(); break; case KeyMetaB: editFn(&edit, EditPrevWord); break; case KeyMetaD: editFn(&edit, EditDeleteNextWord); break; case KeyMetaF: editFn(&edit, EditNextWord); break; case KeyMetaL: windowBare(); break; case KeyMetaM: uiWrite(windowID(), Warm, NULL, ""); break; case KeyMetaN: windowScroll(ScrollHot, +1); break; case KeyMetaP: windowScroll(ScrollHot, -1); break; case KeyMetaQ: editFn(&edit, EditCollapse); break; case KeyMetaS: spoilerReveal ^= true; windowUpdate(); break; case KeyMetaT: windowToggleTime(); break; case KeyMetaU: windowScroll(ScrollUnread, 0); break; case KeyMetaV: windowScroll(ScrollPage, +1); break; case KeyCtrlLeft: editFn(&edit, EditPrevWord); break; case KeyCtrlRight: editFn(&edit, EditNextWord); break; case KEY_BACKSPACE: editFn(&edit, EditDeletePrev); break; case KEY_DC: editFn(&edit, EditDeleteNext); break; case KEY_DOWN: windowScroll(ScrollOne, -1); break; case KEY_END: editFn(&edit, EditTail); break; case KEY_ENTER: inputEnter(); break; case KEY_HOME: editFn(&edit, EditHead); break; case KEY_LEFT: editFn(&edit, EditPrev); break; case KEY_NPAGE: windowScroll(ScrollPage, -1); break; case KEY_PPAGE: windowScroll(ScrollPage, +1); break; case KEY_RIGHT: editFn(&edit, EditNext); break; case KEY_SEND: windowScroll(ScrollAll, -1); break; case KEY_SHOME: windowScroll(ScrollAll, +1); break; case KEY_UP: windowScroll(ScrollOne, +1); } } static void keyCtrl(wchar_t ch) { switch (ch ^ L'@') { break; case L'?': editFn(&edit, EditDeletePrev); break; case L'A': editFn(&edit, EditHead); break; case L'B': editFn(&edit, EditPrev); break; case L'C': raise(SIGINT); break; case L'D': editFn(&edit, EditDeleteNext); break; case L'E': editFn(&edit, EditTail); break; case L'F': editFn(&edit, EditNext); break; case L'H': editFn(&edit, EditDeletePrev); break; case L'J': inputEnter(); break; case L'K': editFn(&edit, EditDeleteTail); break; case L'L': clearok(curscr, true); break; case L'N': windowShow(windowNum() + 1); break; case L'P': windowShow(windowNum() - 1); break; case L'R': windowSearch(editString(&edit), -1); break; case L'S': windowSearch(editString(&edit), +1); break; case L'T': editFn(&edit, EditTranspose); break; case L'U': editFn(&edit, EditDeleteHead); break; case L'V': windowScroll(ScrollPage, -1); break; case L'W': editFn(&edit, EditDeletePrevWord); break; case L'Y': editFn(&edit, EditPaste); } } static void keyStyle(wchar_t ch) { if (iswcntrl(ch)) ch = towlower(ch ^ L'@'); char buf[8] = {0}; enum Color color = Default; switch (ch) { break; case L'A': color = Gray; break; case L'B': color = Blue; break; case L'C': color = Cyan; break; case L'G': color = Green; break; case L'K': color = Black; break; case L'M': color = Magenta; break; case L'N': color = Brown; break; case L'O': color = Orange; break; case L'P': color = Pink; break; case L'R': color = Red; break; case L'W': color = White; break; case L'Y': color = Yellow; break; case L'b': buf[0] = B; break; case L'c': buf[0] = C; break; case L'i': buf[0] = I; break; case L'o': buf[0] = O; break; case L'r': buf[0] = R; break; case L's': { snprintf(buf, sizeof(buf), "%c%02d,%02d", C, Black, Black); } break; case L'u': buf[0] = U; } if (color != Default) { snprintf(buf, sizeof(buf), "%c%02d", C, color); } for (char *ch = buf; *ch; ++ch) { editInsert(&edit, *ch); } } void uiRead(void) { if (hidden) { if (waiting) { uiShow(); flushinp(); waiting = false; } else { return; } } wint_t ch; static bool paste, style, literal; for (int ret; ERR != (ret = wget_wch(input, &ch));) { bool spr = spoilerReveal; if (ret == KEY_CODE_YES && ch == KeyPasteOn) { paste = true; } else if (ret == KEY_CODE_YES && ch == KeyPasteOff) { paste = false; } else if (ret == KEY_CODE_YES && ch == KeyPasteManual) { paste ^= true; } else if (paste || literal) { editInsert(&edit, ch); } else if (ret == KEY_CODE_YES) { keyCode(ch); } else if (ch == (L'Z' ^ L'@')) { style = true; continue; } else if (style && ch == (L'V' ^ L'@')) { literal = true; continue; } else if (style) { keyStyle(ch); } else if (iswcntrl(ch)) { keyCtrl(ch); } else { editInsert(&edit, ch); } style = false; literal = false; if (spr) { spoilerReveal = false; windowUpdate(); } } uiUpdate(); } static FILE *saveFile; static const uint64_t Signatures[] = { 0x6C72696774616301, // no heat, unread, unreadWarm 0x6C72696774616302, // no self.pos 0x6C72696774616303, // no buffer line heat 0x6C72696774616304, // no mute 0x6C72696774616305, // no URLs 0x6C72696774616306, // no thresh 0x6C72696774616307, // no window time 0x6C72696774616308, }; static size_t signatureVersion(uint64_t signature) { for (size_t i = 0; i < ARRAY_LEN(Signatures); ++i) { if (signature == Signatures[i]) return i; } errx(EX_DATAERR, "unknown file signature %" PRIX64, signature); } static int writeUint64(FILE *file, uint64_t u) { return (fwrite(&u, sizeof(u), 1, file) ? 0 : -1); } int uiSave(void) { return 0 || ftruncate(fileno(saveFile), 0) || writeUint64(saveFile, Signatures[7]) || writeUint64(saveFile, self.pos) || windowSave(saveFile) || urlSave(saveFile) || fclose(saveFile); } static uint64_t readUint64(FILE *file) { uint64_t u; fread(&u, sizeof(u), 1, file); if (ferror(file)) err(EX_IOERR, "fread"); if (feof(file)) errx(EX_DATAERR, "unexpected eof"); return u; } void uiLoad(const char *name) { int error; saveFile = dataOpen(name, "a+e"); if (!saveFile) exit(EX_CANTCREAT); rewind(saveFile); #ifdef __FreeBSD__ cap_rights_t rights; cap_rights_init(&rights, CAP_READ, CAP_WRITE, CAP_FLOCK, CAP_FTRUNCATE); error = caph_rights_limit(fileno(saveFile), &rights); if (error) err(EX_OSERR, "cap_rights_limit"); #endif error = flock(fileno(saveFile), LOCK_EX | LOCK_NB); if (error && errno == EWOULDBLOCK) { errx(EX_CANTCREAT, "%s: save file in use", name); } time_t signature; fread(&signature, sizeof(signature), 1, saveFile); if (ferror(saveFile)) err(EX_IOERR, "fread"); if (feof(saveFile)) { return; } size_t version = signatureVersion(signature); if (version > 1) { self.pos = readUint64(saveFile); } windowLoad(saveFile, version); urlLoad(saveFile, version); }