/* Copyright (C) 2018 Curtis McEnroe * * 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 . */ #define _XOPEN_SOURCE_EXTENDED #include #include #include #include #include #include #include #include #include #include #include #include "chat.h" #undef uiFmt #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define CTRL(c) ((c) & 037) #define UNCTRL(c) ((c) + '@') #ifndef A_ITALIC #define A_ITALIC A_NORMAL #endif static void focusEnable(void) { printf("\33[?1004h"); fflush(stdout); } static void focusDisable(void) { printf("\33[?1004l"); fflush(stdout); } static void colorInit(void) { start_color(); use_default_colors(); if (COLORS >= 16) { for (short pair = 0; pair < 0x100; ++pair) { if (pair < 0x10) { init_pair(1 + pair, pair, -1); } else { init_pair(1 + pair, pair & 0x0F, (pair & 0xF0) >> 4); } } } else { for (short pair = 0; pair < 0100; ++pair) { if (pair < 010) { init_pair(1 + pair, pair, -1); } else { init_pair(1 + pair, pair & 007, (pair & 070) >> 3); } } } } static attr_t attr8(short pair) { if (COLORS >= 16 || pair < 0) return A_NORMAL; return (pair & 0x08) ? A_BOLD : A_NORMAL; } static short pair8(short pair) { if (COLORS >= 16 || pair < 0) return pair; return (pair & 0x70) >> 1 | (pair & 0x07); } static const int TOPIC_COLS = 512; static const int INPUT_COLS = 512; static const int LOG_LINES = 100; static int lastLine(void) { return LINES - 1; } static int lastCol(void) { return COLS - 1; } static int logHeight(void) { return LINES - 4; } struct View { WINDOW *topic; WINDOW *log; 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) { setlocale(LC_CTYPE, ""); initscr(); cbreak(); noecho(); focusEnable(); colorInit(); ui.tag = TAG_DEFAULT; ui.input = newpad(2, INPUT_COLS); mvwhline(ui.input, 0, 0, ACS_HLINE, INPUT_COLS); wmove(ui.input, 1, 0); keypad(ui.input, true); nodelay(ui.input, true); } void uiHide(void) { ui.hide = true; endwin(); } void uiExit(void) { uiHide(); focusDisable(); printf( "This program is AGPLv3 free software!\n" "The source is available at <" SOURCE_URL ">.\n" ); } 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( view->topic, 0, 0, 0, 0, 1, lastCol() ); pnoutrefresh( view->log, view->scroll - logHeight(), 0, 2, 0, lastLine() - 2, lastCol() ); int _, x; getyx(ui.input, _, x); pnoutrefresh( ui.input, 0, MAX(0, x - lastCol() + 3), lastLine() - 1, 0, lastLine(), lastCol() ); doupdate(); } 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 } static const short IRC_COLORS[16] = { 8 + COLOR_WHITE, // white 0 + COLOR_BLACK, // black 0 + COLOR_BLUE, // blue 0 + COLOR_GREEN, // green 8 + COLOR_RED, // red 0 + COLOR_RED, // brown 0 + COLOR_MAGENTA, // magenta 0 + COLOR_YELLOW, // orange 8 + COLOR_YELLOW, // yellow 8 + COLOR_GREEN, // light green 0 + COLOR_CYAN, // cyan 8 + COLOR_CYAN, // light cyan 8 + COLOR_BLUE, // light blue 8 + COLOR_MAGENTA, // pink 8 + COLOR_BLACK, // gray 0 + COLOR_WHITE, // light gray }; static const wchar_t *parseColor(short *pair, const wchar_t *str) { short fg = 0; size_t fgLen = MIN(wcsspn(str, L"0123456789"), 2); if (!fgLen) { *pair = -1; return str; } for (size_t i = 0; i < fgLen; ++i) { fg = fg * 10 + (str[i] - L'0'); } str = &str[fgLen]; short bg = 0; size_t bgLen = 0; if (str[0] == L',') bgLen = MIN(wcsspn(&str[1], L"0123456789"), 2); for (size_t i = 0; i < bgLen; ++i) { bg = bg * 10 + (str[1 + i] - L'0'); } if (bgLen) str = &str[1 + bgLen]; if (*pair == -1) *pair = 0; *pair = (*pair & 0xF0) | IRC_COLORS[fg & 0x0F]; if (bgLen) *pair = (*pair & 0x0F) | (IRC_COLORS[bg & 0x0F] << 4); return str; } static void wordWrap(WINDOW *win, const wchar_t *str) { size_t len = wcscspn(str, L" "); size_t width = 1; for (size_t i = 0; i < len; ++i) { if (iswprint(str[i])) width += wcwidth(str[i]); } int _, x, xMax; getyx(win, _, x); getmaxyx(win, _, xMax); if (width >= (size_t)(xMax - x)) { waddch(win, '\n'); } else { waddch(win, ' '); } } static const wchar_t IRC_CODES[] = { L' ', IRC_BOLD, IRC_COLOR, IRC_REVERSE, IRC_RESET, IRC_ITALIC, IRC_UNDERLINE, L'\0', }; static void addIRC(WINDOW *win, const wchar_t *str) { attr_t attr = A_NORMAL; short pair = -1; for (;;) { size_t cc = wcscspn(str, IRC_CODES); wattr_set(win, attr | attr8(pair), 1 + pair8(pair), NULL); waddnwstr(win, str, cc); if (!str[cc]) break; str = &str[cc]; switch (*str++) { break; case L' ': wordWrap(win, str); break; case IRC_BOLD: attr ^= A_BOLD; break; case IRC_ITALIC: attr ^= A_ITALIC; break; case IRC_UNDERLINE: attr ^= A_UNDERLINE; break; case IRC_REVERSE: attr ^= A_REVERSE; break; case IRC_COLOR: str = parseColor(&pair, str); break; case IRC_RESET: attr = A_NORMAL; pair = -1; } } } void uiTopic(struct Tag tag, const char *topic) { wchar_t *wcs = ambstowcs(topic); if (!wcs) err(EX_DATAERR, "ambstowcs"); struct View *view = uiView(tag); wmove(view->topic, 0, 0); addIRC(view->topic, wcs); wclrtoeol(view->topic); free(wcs); } 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(view->log, line); } 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(tag, buf); free(buf); } static void logUp(void) { 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) { 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(wchar_t ch) { static bool esc, csi; if (ch == L'\33') { esc = true; return false; } if (esc && ch == L'[') { esc = false; csi = true; return false; } 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; return update; } switch (ch) { 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 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) { ui.hide = false; bool update = false; int ret; wint_t ch; while (ERR != (ret = wget_wch(ui.input, &ch))) { if (ret == KEY_CODE_YES) { update |= keyCode(ch); } else { update |= keyChar(ch); } } if (!update) return; wmove(ui.input, 1, 0); addIRC(ui.input, editHead()); int y, x; getyx(ui.input, y, x); addIRC(ui.input, editTail()); wclrtoeol(ui.input); wmove(ui.input, y, x); }