IRC client
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ui.c 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. /* Copyright (C) 2018 Curtis McEnroe <june@causal.agency>
  2. *
  3. * This program is free software: you can redistribute it and/or modify
  4. * it under the terms of the GNU Affero General Public License as published by
  5. * the Free Software Foundation, either version 3 of the License, or
  6. * (at your option) any later version.
  7. *
  8. * This program is distributed in the hope that it will be useful,
  9. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. * GNU Affero General Public License for more details.
  12. *
  13. * You should have received a copy of the GNU Affero General Public License
  14. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. */
  16. #define _XOPEN_SOURCE_EXTENDED
  17. #include <curses.h>
  18. #include <err.h>
  19. #include <locale.h>
  20. #include <stdarg.h>
  21. #include <stdbool.h>
  22. #include <stdlib.h>
  23. #include <string.h>
  24. #include <sysexits.h>
  25. #include <wchar.h>
  26. #include <wctype.h>
  27. #ifndef A_ITALIC
  28. #define A_ITALIC A_NORMAL
  29. #endif
  30. #include "chat.h"
  31. #undef uiFmt
  32. static void colorInit(void) {
  33. start_color();
  34. use_default_colors();
  35. if (COLORS >= 16) {
  36. for (short pair = 0; pair < 0x100; ++pair) {
  37. if (pair < 0x10) {
  38. init_pair(1 + pair, pair, -1);
  39. } else {
  40. init_pair(1 + pair, pair & 0x0F, (pair & 0xF0) >> 4);
  41. }
  42. }
  43. } else {
  44. for (short pair = 0; pair < 0100; ++pair) {
  45. if (pair < 010) {
  46. init_pair(1 + pair, pair, -1);
  47. } else {
  48. init_pair(1 + pair, pair & 007, (pair & 070) >> 3);
  49. }
  50. }
  51. }
  52. }
  53. static attr_t attr8(short pair) {
  54. if (COLORS >= 16 || pair < 0) return A_NORMAL;
  55. return (pair & 0x08) ? A_BOLD : A_NORMAL;
  56. }
  57. static short pair8(short pair) {
  58. if (COLORS >= 16 || pair < 0) return pair;
  59. return (pair & 0x70) >> 1 | (pair & 0x07);
  60. }
  61. struct View {
  62. struct Tag tag;
  63. WINDOW *log;
  64. int scroll;
  65. bool hot, mark;
  66. uint unread;
  67. struct View *prev;
  68. struct View *next;
  69. };
  70. static struct {
  71. bool hide;
  72. WINDOW *status;
  73. WINDOW *input;
  74. struct View *view;
  75. } ui;
  76. void uiShow(void) {
  77. ui.hide = false;
  78. termMode(TermFocus, true);
  79. uiDraw();
  80. }
  81. void uiHide(void) {
  82. ui.hide = true;
  83. termMode(TermFocus, false);
  84. endwin();
  85. }
  86. void uiInit(void) {
  87. setlocale(LC_CTYPE, "");
  88. initscr();
  89. cbreak();
  90. noecho();
  91. colorInit();
  92. termInit();
  93. ui.status = newwin(1, COLS, 0, 0);
  94. ui.input = newpad(1, 512);
  95. keypad(ui.input, true);
  96. nodelay(ui.input, true);
  97. uiViewTag(TagStatus);
  98. uiShow();
  99. }
  100. void uiExit(void) {
  101. uiHide();
  102. printf(
  103. "This program is AGPLv3 Free Software!\n"
  104. "The source is available at <" SOURCE_URL ">.\n"
  105. );
  106. }
  107. static int lastLine(void) {
  108. return LINES - 1;
  109. }
  110. static int lastCol(void) {
  111. return COLS - 1;
  112. }
  113. static int logHeight(void) {
  114. return LINES - 2;
  115. }
  116. void uiDraw(void) {
  117. if (ui.hide) return;
  118. wnoutrefresh(ui.status);
  119. pnoutrefresh(
  120. ui.view->log,
  121. ui.view->scroll - logHeight(), 0,
  122. 1, 0,
  123. lastLine() - 1, lastCol()
  124. );
  125. int _, x;
  126. getyx(ui.input, _, x);
  127. pnoutrefresh(
  128. ui.input,
  129. 0, MAX(0, x - lastCol() + 3),
  130. lastLine(), 0,
  131. lastLine(), lastCol()
  132. );
  133. doupdate();
  134. }
  135. static const short Colors[] = {
  136. [IRCWhite] = 8 + COLOR_WHITE,
  137. [IRCBlack] = 0 + COLOR_BLACK,
  138. [IRCBlue] = 0 + COLOR_BLUE,
  139. [IRCGreen] = 0 + COLOR_GREEN,
  140. [IRCRed] = 8 + COLOR_RED,
  141. [IRCBrown] = 0 + COLOR_RED,
  142. [IRCMagenta] = 0 + COLOR_MAGENTA,
  143. [IRCOrange] = 0 + COLOR_YELLOW,
  144. [IRCYellow] = 8 + COLOR_YELLOW,
  145. [IRCLightGreen] = 8 + COLOR_GREEN,
  146. [IRCCyan] = 0 + COLOR_CYAN,
  147. [IRCLightCyan] = 8 + COLOR_CYAN,
  148. [IRCLightBlue] = 8 + COLOR_BLUE,
  149. [IRCPink] = 8 + COLOR_MAGENTA,
  150. [IRCGray] = 8 + COLOR_BLACK,
  151. [IRCLightGray] = 0 + COLOR_WHITE,
  152. };
  153. static void addFormat(WINDOW *win, const struct Format *format) {
  154. attr_t attr = A_NORMAL;
  155. if (format->bold) attr |= A_BOLD;
  156. if (format->italic) attr |= A_ITALIC;
  157. if (format->underline) attr |= A_UNDERLINE;
  158. if (format->reverse) attr |= A_REVERSE;
  159. short pair = -1;
  160. if (format->fg != IRCDefault) pair = Colors[format->fg];
  161. if (format->bg != IRCDefault) pair |= Colors[format->bg] << 4;
  162. wattr_set(win, attr | attr8(pair), 1 + pair8(pair), NULL);
  163. waddnwstr(win, format->str, format->len);
  164. }
  165. static int printWidth(const wchar_t *str, size_t len) {
  166. int width = 0;
  167. for (size_t i = 0; i < len; ++i) {
  168. if (iswprint(str[i])) width += wcwidth(str[i]);
  169. }
  170. return width;
  171. }
  172. static int addWrap(WINDOW *win, const wchar_t *str) {
  173. int lines = 0;
  174. struct Format format = { .str = str };
  175. formatReset(&format);
  176. while (formatParse(&format, NULL)) {
  177. size_t word = 1 + wcscspn(&format.str[1], L" ");
  178. if (word < format.len) format.len = word;
  179. int _, x, xMax;
  180. getyx(win, _, x);
  181. getmaxyx(win, _, xMax);
  182. if (xMax - x - 1 < printWidth(format.str, word)) {
  183. if (format.str[0] == L' ') {
  184. format.str++;
  185. format.len--;
  186. }
  187. waddch(win, '\n');
  188. lines++;
  189. }
  190. addFormat(win, &format);
  191. }
  192. return lines;
  193. }
  194. static struct {
  195. struct View *head;
  196. struct View *tail;
  197. struct View *tags[TagsLen];
  198. } views;
  199. static void uiTitle(const struct View *view) {
  200. int unread;
  201. char *str;
  202. int len = asprintf(
  203. &str, "%s%n (%u)", view->tag.name, &unread, view->unread
  204. );
  205. if (len < 0) err(EX_OSERR, "asprintf");
  206. if (!view->unread) str[unread] = '\0';
  207. termTitle(str);
  208. free(str);
  209. }
  210. static void uiStatus(void) {
  211. wmove(ui.status, 0, 0);
  212. int num = 0;
  213. for (const struct View *view = views.head; view; view = view->next, ++num) {
  214. if (!view->unread && view != ui.view) continue;
  215. if (view == ui.view) uiTitle(view);
  216. int unread;
  217. wchar_t *str;
  218. int len = aswprintf(
  219. &str, L"%c %d %s %n(\3%02d%u\3) ",
  220. (view == ui.view ? IRCReverse : IRCReset),
  221. num, view->tag.name,
  222. &unread, (view->hot ? IRCYellow : IRCDefault), view->unread
  223. );
  224. if (len < 0) err(EX_OSERR, "aswprintf");
  225. if (!view->unread) str[unread] = L'\0';
  226. addWrap(ui.status, str);
  227. free(str);
  228. }
  229. wclrtoeol(ui.status);
  230. }
  231. static void viewAppend(struct View *view) {
  232. if (views.tail) views.tail->next = view;
  233. view->prev = views.tail;
  234. view->next = NULL;
  235. views.tail = view;
  236. if (!views.head) views.head = view;
  237. views.tags[view->tag.id] = view;
  238. }
  239. static void viewRemove(struct View *view) {
  240. if (view->prev) view->prev->next = view->next;
  241. if (view->next) view->next->prev = view->prev;
  242. if (views.head == view) views.head = view->next;
  243. if (views.tail == view) views.tail = view->prev;
  244. views.tags[view->tag.id] = NULL;
  245. }
  246. static const int LogLines = 512;
  247. static struct View *viewTag(struct Tag tag) {
  248. struct View *view = views.tags[tag.id];
  249. if (view) return view;
  250. view = calloc(1, sizeof(*view));
  251. if (!view) err(EX_OSERR, "calloc");
  252. view->tag = tag;
  253. view->log = newpad(LogLines, COLS);
  254. wsetscrreg(view->log, 0, LogLines - 1);
  255. scrollok(view->log, true);
  256. wmove(view->log, LogLines - 1, 0);
  257. view->scroll = LogLines;
  258. view->mark = true;
  259. viewAppend(view);
  260. return view;
  261. }
  262. static void viewClose(struct View *view) {
  263. viewRemove(view);
  264. delwin(view->log);
  265. free(view);
  266. }
  267. static void uiResize(void) {
  268. wresize(ui.status, 1, COLS);
  269. for (struct View *view = views.head; view; view = view->next) {
  270. wresize(view->log, LogLines, COLS);
  271. wmove(view->log, LogLines - 1, lastCol());
  272. }
  273. }
  274. static void viewUnmark(struct View *view) {
  275. view->mark = false;
  276. view->unread = 0;
  277. view->hot = false;
  278. uiStatus();
  279. }
  280. static void uiView(struct View *view) {
  281. touchwin(view->log);
  282. if (ui.view) ui.view->mark = true;
  283. viewUnmark(view);
  284. ui.view = view;
  285. uiStatus();
  286. uiPrompt();
  287. }
  288. void uiViewTag(struct Tag tag) {
  289. uiView(viewTag(tag));
  290. }
  291. void uiViewNum(int num) {
  292. if (num < 0) {
  293. for (struct View *view = views.tail; view; view = view->prev) {
  294. if (++num) continue;
  295. uiView(view);
  296. break;
  297. }
  298. } else {
  299. for (struct View *view = views.head; view; view = view->next) {
  300. if (num--) continue;
  301. uiView(view);
  302. break;
  303. }
  304. }
  305. }
  306. void uiCloseTag(struct Tag tag) {
  307. struct View *view = viewTag(tag);
  308. if (ui.view == view) {
  309. if (view->next) {
  310. uiView(view->next);
  311. } else if (view->prev) {
  312. uiView(view->prev);
  313. } else {
  314. return;
  315. }
  316. }
  317. viewClose(view);
  318. }
  319. static void notify(struct Tag tag, const wchar_t *line) {
  320. beep();
  321. if (!self.notify) return;
  322. char buf[256];
  323. size_t cap = sizeof(buf);
  324. struct Format format = { .str = line };
  325. formatReset(&format);
  326. while (formatParse(&format, NULL)) {
  327. int len = snprintf(
  328. &buf[sizeof(buf) - cap], cap,
  329. "%.*ls", (int)format.len, format.str
  330. );
  331. if (len < 0) err(EX_OSERR, "snprintf");
  332. if ((size_t)len >= cap) break;
  333. cap -= len;
  334. }
  335. eventPipe((const char *[]) { "notify-send", tag.name, buf, NULL });
  336. }
  337. void uiLog(struct Tag tag, enum UIHeat heat, const wchar_t *line) {
  338. struct View *view = viewTag(tag);
  339. int lines = 1;
  340. waddch(view->log, '\n');
  341. if (view->mark && heat > UICold) {
  342. if (!view->unread++) {
  343. lines++;
  344. waddch(view->log, '\n');
  345. }
  346. if (heat > UIWarm) {
  347. view->hot = true;
  348. notify(tag, line);
  349. }
  350. uiStatus();
  351. }
  352. lines += addWrap(view->log, line);
  353. if (view->scroll != LogLines) view->scroll -= lines;
  354. }
  355. void uiFmt(struct Tag tag, enum UIHeat heat, const wchar_t *format, ...) {
  356. wchar_t *str;
  357. va_list ap;
  358. va_start(ap, format);
  359. vaswprintf(&str, format, ap);
  360. va_end(ap);
  361. if (!str) err(EX_OSERR, "vaswprintf");
  362. uiLog(tag, heat, str);
  363. free(str);
  364. }
  365. static void scrollUp(int lines) {
  366. if (ui.view->scroll == logHeight()) return;
  367. if (ui.view->scroll == LogLines) ui.view->mark = true;
  368. ui.view->scroll = MAX(ui.view->scroll - lines, logHeight());
  369. }
  370. static void scrollDown(int lines) {
  371. if (ui.view->scroll == LogLines) return;
  372. ui.view->scroll = MIN(ui.view->scroll + lines, LogLines);
  373. if (ui.view->scroll == LogLines) viewUnmark(ui.view);
  374. }
  375. static void keyCode(wchar_t ch) {
  376. switch (ch) {
  377. break; case KEY_RESIZE: uiResize();
  378. break; case KEY_SLEFT: scrollUp(1);
  379. break; case KEY_SRIGHT: scrollDown(1);
  380. break; case KEY_PPAGE: scrollUp(logHeight() / 2);
  381. break; case KEY_NPAGE: scrollDown(logHeight() / 2);
  382. break; case KEY_LEFT: edit(ui.view->tag, EditLeft, 0);
  383. break; case KEY_RIGHT: edit(ui.view->tag, EditRight, 0);
  384. break; case KEY_HOME: edit(ui.view->tag, EditHome, 0);
  385. break; case KEY_END: edit(ui.view->tag, EditEnd, 0);
  386. break; case KEY_DC: edit(ui.view->tag, EditDelete, 0);
  387. break; case KEY_BACKSPACE: edit(ui.view->tag, EditBackspace, 0);
  388. break; case KEY_ENTER: edit(ui.view->tag, EditEnter, 0);
  389. }
  390. }
  391. #define CTRL(ch) ((ch) ^ 0100)
  392. static void keyChar(wchar_t ch) {
  393. if (ch < 0200) {
  394. enum TermEvent event = termEvent((char)ch);
  395. switch (event) {
  396. break; case TermFocusIn: viewUnmark(ui.view);
  397. break; case TermFocusOut: ui.view->mark = true;
  398. break; default: {}
  399. }
  400. if (event) return;
  401. }
  402. static bool meta;
  403. if (ch == L'\33') {
  404. meta = true;
  405. return;
  406. }
  407. if (ch == L'\177') ch = L'\b';
  408. if (meta) {
  409. meta = false;
  410. switch (ch) {
  411. break; case L'b': edit(ui.view->tag, EditBackWord, 0);
  412. break; case L'f': edit(ui.view->tag, EditForeWord, 0);
  413. break; case L'\b': edit(ui.view->tag, EditKillBackWord, 0);
  414. break; case L'd': edit(ui.view->tag, EditKillForeWord, 0);
  415. break; case L'm': uiLog(ui.view->tag, UICold, L"");
  416. break; default: {
  417. if (ch >= L'0' && ch <= L'9') uiViewNum(ch - L'0');
  418. }
  419. }
  420. return;
  421. }
  422. switch (ch) {
  423. break; case CTRL(L'L'): clearok(curscr, true);
  424. break; case CTRL(L'A'): edit(ui.view->tag, EditHome, 0);
  425. break; case CTRL(L'B'): edit(ui.view->tag, EditLeft, 0);
  426. break; case CTRL(L'D'): edit(ui.view->tag, EditDelete, 0);
  427. break; case CTRL(L'E'): edit(ui.view->tag, EditEnd, 0);
  428. break; case CTRL(L'F'): edit(ui.view->tag, EditRight, 0);
  429. break; case CTRL(L'K'): edit(ui.view->tag, EditKillLine, 0);
  430. break; case CTRL(L'W'): edit(ui.view->tag, EditKillBackWord, 0);
  431. break; case CTRL(L'C'): edit(ui.view->tag, EditInsert, IRCColor);
  432. break; case CTRL(L'N'): edit(ui.view->tag, EditInsert, IRCReset);
  433. break; case CTRL(L'O'): edit(ui.view->tag, EditInsert, IRCBold);
  434. break; case CTRL(L'R'): edit(ui.view->tag, EditInsert, IRCColor);
  435. break; case CTRL(L'T'): edit(ui.view->tag, EditInsert, IRCItalic);
  436. break; case CTRL(L'U'): edit(ui.view->tag, EditInsert, IRCUnderline);
  437. break; case CTRL(L'V'): edit(ui.view->tag, EditInsert, IRCReverse);
  438. break; case L'\b': edit(ui.view->tag, EditBackspace, 0);
  439. break; case L'\t': edit(ui.view->tag, EditComplete, 0);
  440. break; case L'\n': edit(ui.view->tag, EditEnter, 0);
  441. break; default: {
  442. if (iswprint(ch)) edit(ui.view->tag, EditInsert, ch);
  443. }
  444. }
  445. }
  446. static bool isAction(struct Tag tag, const wchar_t *input) {
  447. if (tag.id == TagStatus.id || tag.id == TagRaw.id) return false;
  448. return !wcsncasecmp(input, L"/me ", 4);
  449. }
  450. // FIXME: This duplicates logic from input.c for wcs.
  451. static bool isCommand(struct Tag tag, const wchar_t *input) {
  452. if (tag.id == TagStatus.id || tag.id == TagRaw.id) return true;
  453. if (input[0] != L'/') return false;
  454. const wchar_t *space = wcschr(&input[1], L' ');
  455. const wchar_t *extra = wcschr(&input[1], L'/');
  456. return !extra || (space && extra > space);
  457. }
  458. void uiPrompt(void) {
  459. const wchar_t *input = editHead();
  460. // TODO: Avoid reformatting these on every read.
  461. wchar_t *prompt = NULL;
  462. int len = 0;
  463. if (isAction(ui.view->tag, input) && editTail() >= &input[4]) {
  464. input = &input[4];
  465. len = aswprintf(
  466. &prompt, L"\3%d* %s\3 ",
  467. formatColor(self.user), self.nick
  468. );
  469. } else if (!isCommand(ui.view->tag, input)) {
  470. len = aswprintf(
  471. &prompt, L"\3%d(%s)\3 ",
  472. formatColor(self.user), self.nick
  473. );
  474. }
  475. if (len < 0) err(EX_OSERR, "aswprintf");
  476. wmove(ui.input, 0, 0);
  477. if (prompt) addWrap(ui.input, prompt);
  478. free(prompt);
  479. int _, x = 0;
  480. struct Format format = { .str = input };
  481. formatReset(&format);
  482. while (formatParse(&format, editTail())) {
  483. if (format.split) getyx(ui.input, _, x);
  484. addFormat(ui.input, &format);
  485. }
  486. wclrtoeol(ui.input);
  487. wmove(ui.input, 0, x);
  488. }
  489. void uiRead(void) {
  490. if (ui.hide) uiShow();
  491. int ret;
  492. wint_t ch;
  493. while (ERR != (ret = wget_wch(ui.input, &ch))) {
  494. if (ret == KEY_CODE_YES) {
  495. keyCode(ch);
  496. } else {
  497. keyChar(ch);
  498. }
  499. }
  500. uiPrompt();
  501. }