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 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. /* Copyright (C) 2018, 2019 C. 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 <stdarg.h>
  20. #include <stdbool.h>
  21. #include <stdlib.h>
  22. #include <string.h>
  23. #include <sysexits.h>
  24. #include <wchar.h>
  25. #include <wctype.h>
  26. #ifndef A_ITALIC
  27. #define A_ITALIC A_UNDERLINE
  28. #endif
  29. #include "chat.h"
  30. #undef uiFmt
  31. #define CTRL(ch) ((ch) & 037)
  32. enum { Esc = L'\33', Del = L'\177' };
  33. static const int LogLines = 512;
  34. static int lastLine(void) {
  35. return LINES - 1;
  36. }
  37. static int lastCol(void) {
  38. return COLS - 1;
  39. }
  40. static int logHeight(void) {
  41. return LINES - 2;
  42. }
  43. struct Window {
  44. struct Tag tag;
  45. WINDOW *log;
  46. bool hot;
  47. bool mark;
  48. int scroll;
  49. uint unread;
  50. struct Window *prev;
  51. struct Window *next;
  52. };
  53. static struct {
  54. struct Window *active;
  55. struct Window *other;
  56. struct Window *head;
  57. struct Window *tail;
  58. struct Window *tag[TagsLen];
  59. } windows;
  60. static void windowAppend(struct Window *win) {
  61. if (windows.tail) windows.tail->next = win;
  62. win->prev = windows.tail;
  63. win->next = NULL;
  64. windows.tail = win;
  65. if (!windows.head) windows.head = win;
  66. windows.tag[win->tag.id] = win;
  67. }
  68. static void windowInsert(struct Window *win, struct Window *next) {
  69. win->prev = next->prev;
  70. win->next = next;
  71. if (win->prev) win->prev->next = win;
  72. win->next->prev = win;
  73. if (!win->prev) windows.head = win;
  74. windows.tag[win->tag.id] = win;
  75. }
  76. static void windowRemove(struct Window *win) {
  77. windows.tag[win->tag.id] = NULL;
  78. if (win->prev) win->prev->next = win->next;
  79. if (win->next) win->next->prev = win->prev;
  80. if (windows.head == win) windows.head = win->next;
  81. if (windows.tail == win) windows.tail = win->prev;
  82. }
  83. static struct Window *windowFor(struct Tag tag) {
  84. struct Window *win = windows.tag[tag.id];
  85. if (win) {
  86. win->tag = tag;
  87. return win;
  88. }
  89. win = calloc(1, sizeof(*win));
  90. if (!win) err(EX_OSERR, "calloc");
  91. win->tag = tag;
  92. win->mark = true;
  93. win->scroll = LogLines;
  94. win->log = newpad(LogLines, COLS);
  95. wsetscrreg(win->log, 0, LogLines - 1);
  96. scrollok(win->log, true);
  97. wmove(win->log, LogLines - 1, 0);
  98. windowAppend(win);
  99. return win;
  100. }
  101. static void windowResize(struct Window *win) {
  102. wresize(win->log, LogLines, COLS);
  103. wmove(win->log, LogLines - 1, lastCol());
  104. }
  105. static void windowMark(struct Window *win) {
  106. win->mark = true;
  107. }
  108. static void windowUnmark(struct Window *win) {
  109. win->mark = false;
  110. win->unread = 0;
  111. win->hot = false;
  112. }
  113. static void windowShow(struct Window *win) {
  114. if (windows.active) windowMark(windows.active);
  115. if (win) {
  116. touchwin(win->log);
  117. windowUnmark(win);
  118. }
  119. windows.other = windows.active;
  120. windows.active = win;
  121. }
  122. static void windowClose(struct Window *win) {
  123. if (windows.active == win) windowShow(win->next ? win->next : win->prev);
  124. if (windows.other == win) windows.other = NULL;
  125. windowRemove(win);
  126. delwin(win->log);
  127. free(win);
  128. }
  129. static void windowScroll(struct Window *win, int lines) {
  130. if (lines < 0) {
  131. if (win->scroll == logHeight()) return;
  132. if (win->scroll == LogLines) windowMark(win);
  133. win->scroll = MAX(win->scroll + lines, logHeight());
  134. } else {
  135. if (win->scroll == LogLines) return;
  136. win->scroll = MIN(win->scroll + lines, LogLines);
  137. if (win->scroll == LogLines) windowUnmark(win);
  138. }
  139. }
  140. static void colorInit(void) {
  141. start_color();
  142. use_default_colors();
  143. if (COLORS < 16) {
  144. for (short pair = 0; pair < 0100; ++pair) {
  145. if (pair < 010) {
  146. init_pair(1 + pair, pair, -1);
  147. } else {
  148. init_pair(1 + pair, pair & 007, (pair & 070) >> 3);
  149. }
  150. }
  151. } else {
  152. for (short pair = 0; pair < 0x100; ++pair) {
  153. if (pair < 0x10) {
  154. init_pair(1 + pair, pair, -1);
  155. } else {
  156. init_pair(1 + pair, pair & 0x0F, (pair & 0xF0) >> 4);
  157. }
  158. }
  159. }
  160. }
  161. static attr_t colorAttr(short color) {
  162. if (color < 0) return A_NORMAL;
  163. if (COLORS < 16 && (color & 0x08)) return A_BOLD;
  164. return A_NORMAL;
  165. }
  166. static short colorPair(short color) {
  167. if (color < 0) return 0;
  168. if (COLORS < 16) return 1 + ((color & 0x70) >> 1 | (color & 0x07));
  169. return 1 + color;
  170. }
  171. static struct {
  172. bool hide;
  173. WINDOW *status;
  174. WINDOW *input;
  175. } ui;
  176. void uiInit(void) {
  177. initscr();
  178. cbreak();
  179. noecho();
  180. termInit();
  181. termNoFlow();
  182. def_prog_mode();
  183. colorInit();
  184. ui.status = newwin(1, COLS, 0, 0);
  185. ui.input = newpad(1, 512);
  186. keypad(ui.input, true);
  187. nodelay(ui.input, true);
  188. uiShow();
  189. }
  190. static void uiResize(void) {
  191. wresize(ui.status, 1, COLS);
  192. for (struct Window *win = windows.head; win; win = win->next) {
  193. windowResize(win);
  194. }
  195. }
  196. void uiShow(void) {
  197. ui.hide = false;
  198. termMode(TermFocus, true);
  199. uiDraw();
  200. }
  201. void uiHide(void) {
  202. ui.hide = true;
  203. termMode(TermFocus, false);
  204. endwin();
  205. }
  206. void uiExit(int status) {
  207. uiHide();
  208. printf(
  209. "This program is AGPLv3 Free Software!\n"
  210. "Code is available from <" SOURCE_URL ">.\n"
  211. );
  212. exit(status);
  213. }
  214. static int _;
  215. void uiDraw(void) {
  216. if (ui.hide) return;
  217. wnoutrefresh(ui.status);
  218. if (windows.active) {
  219. pnoutrefresh(
  220. windows.active->log,
  221. windows.active->scroll - logHeight(), 0,
  222. 1, 0,
  223. lastLine() - 1, lastCol()
  224. );
  225. }
  226. int x;
  227. getyx(ui.input, _, x);
  228. pnoutrefresh(
  229. ui.input,
  230. 0, MAX(0, x - lastCol() + 3),
  231. lastLine(), 0,
  232. lastLine(), lastCol()
  233. );
  234. doupdate();
  235. }
  236. static const short Colors[] = {
  237. [IRCWhite] = 8 + COLOR_WHITE,
  238. [IRCBlack] = 0 + COLOR_BLACK,
  239. [IRCBlue] = 0 + COLOR_BLUE,
  240. [IRCGreen] = 0 + COLOR_GREEN,
  241. [IRCRed] = 8 + COLOR_RED,
  242. [IRCBrown] = 0 + COLOR_RED,
  243. [IRCMagenta] = 0 + COLOR_MAGENTA,
  244. [IRCOrange] = 0 + COLOR_YELLOW,
  245. [IRCYellow] = 8 + COLOR_YELLOW,
  246. [IRCLightGreen] = 8 + COLOR_GREEN,
  247. [IRCCyan] = 0 + COLOR_CYAN,
  248. [IRCLightCyan] = 8 + COLOR_CYAN,
  249. [IRCLightBlue] = 8 + COLOR_BLUE,
  250. [IRCPink] = 8 + COLOR_MAGENTA,
  251. [IRCGray] = 8 + COLOR_BLACK,
  252. [IRCLightGray] = 0 + COLOR_WHITE,
  253. };
  254. static void addFormat(WINDOW *win, const struct Format *format) {
  255. attr_t attr = A_NORMAL;
  256. if (format->bold) attr |= A_BOLD;
  257. if (format->italic) attr |= A_ITALIC;
  258. if (format->underline) attr |= A_UNDERLINE;
  259. if (format->reverse) attr |= A_REVERSE;
  260. short color = -1;
  261. if (format->fg != IRCDefault) color = Colors[format->fg];
  262. if (format->bg != IRCDefault) color |= Colors[format->bg] << 4;
  263. wattr_set(win, attr | colorAttr(color), colorPair(color), NULL);
  264. waddnwstr(win, format->str, format->len);
  265. }
  266. static int printWidth(const wchar_t *str, size_t len) {
  267. int width = 0;
  268. for (size_t i = 0; i < len; ++i) {
  269. if (iswprint(str[i])) width += wcwidth(str[i]);
  270. }
  271. return width;
  272. }
  273. static int addWrap(WINDOW *win, const wchar_t *str) {
  274. int lines = 0;
  275. struct Format format = { .str = str };
  276. formatReset(&format);
  277. while (formatParse(&format, NULL)) {
  278. size_t word = 1 + wcscspn(&format.str[1], L" ");
  279. if (word < format.len) format.len = word;
  280. int x, xMax;
  281. getyx(win, _, x);
  282. getmaxyx(win, _, xMax);
  283. if (xMax - x - 1 < printWidth(format.str, word)) {
  284. if (format.str[0] == L' ') {
  285. format.str++;
  286. format.len--;
  287. }
  288. waddch(win, '\n');
  289. lines++;
  290. }
  291. addFormat(win, &format);
  292. }
  293. return lines;
  294. }
  295. static void title(const struct Window *win) {
  296. int unread;
  297. char *str;
  298. int len = asprintf(&str, "%s%n (%u)", win->tag.name, &unread, win->unread);
  299. if (len < 0) err(EX_OSERR, "asprintf");
  300. if (!win->unread) str[unread] = '\0';
  301. termTitle(str);
  302. free(str);
  303. }
  304. static void uiStatus(void) {
  305. wmove(ui.status, 0, 0);
  306. int num = 0;
  307. for (const struct Window *win = windows.head; win; win = win->next, ++num) {
  308. if (!win->unread && windows.active != win) continue;
  309. if (windows.active == win) title(win);
  310. int unread;
  311. wchar_t *str;
  312. int len = aswprintf(
  313. &str, L"%c\3%d %d %s %n(\3%02d%u\3%d) ",
  314. (windows.active == win ? IRCReverse : IRCReset), colorFor(win->tag),
  315. num, win->tag.name,
  316. &unread, (win->hot ? IRCWhite : colorFor(win->tag)), win->unread,
  317. colorFor(win->tag)
  318. );
  319. if (len < 0) err(EX_OSERR, "aswprintf");
  320. if (!win->unread) str[unread] = L'\0';
  321. addWrap(ui.status, str);
  322. free(str);
  323. }
  324. wclrtoeol(ui.status);
  325. }
  326. static void uiShowWindow(struct Window *win) {
  327. windowShow(win);
  328. uiStatus();
  329. uiPrompt(false);
  330. }
  331. void uiShowTag(struct Tag tag) {
  332. uiShowWindow(windowFor(tag));
  333. }
  334. static void uiShowAuto(void) {
  335. struct Window *unread = NULL;
  336. struct Window *hot;
  337. for (hot = windows.head; hot; hot = hot->next) {
  338. if (hot->hot) break;
  339. if (!unread && hot->unread) unread = hot;
  340. }
  341. if (!hot && !unread) return;
  342. uiShowWindow(hot ? hot : unread);
  343. }
  344. void uiShowNum(int num, bool relative) {
  345. struct Window *win = (relative ? windows.active : windows.head);
  346. if (num < 0) {
  347. for (; win; win = win->prev) if (!num++) break;
  348. } else {
  349. for (; win; win = win->next) if (!num--) break;
  350. }
  351. if (win) uiShowWindow(win);
  352. }
  353. void uiMoveTag(struct Tag tag, int num, bool relative) {
  354. struct Window *win = windowFor(tag);
  355. windowRemove(win);
  356. struct Window *ins = (relative ? win : windows.head);
  357. if (num < 0) {
  358. for (; ins; ins = ins->prev) if (!num++) break;
  359. } else {
  360. if (relative) ins = ins->next;
  361. for (; ins; ins = ins->next) if (!num--) break;
  362. }
  363. ins ? windowInsert(win, ins) : windowAppend(win);
  364. uiStatus();
  365. }
  366. void uiCloseTag(struct Tag tag) {
  367. windowClose(windowFor(tag));
  368. uiStatus();
  369. uiPrompt(false);
  370. }
  371. static void notify(struct Tag tag, const wchar_t *str) {
  372. beep();
  373. if (!self.notify) return;
  374. size_t len = 0;
  375. char buf[256];
  376. struct Format format = { .str = str };
  377. formatReset(&format);
  378. while (formatParse(&format, NULL)) {
  379. int n = snprintf(
  380. &buf[len], sizeof(buf) - len,
  381. "%.*ls", (int)format.len, format.str
  382. );
  383. if (n < 0) err(EX_OSERR, "snprintf");
  384. len += n;
  385. if (len >= sizeof(buf)) break;
  386. }
  387. eventPipe((const char *[]) { "notify-send", tag.name, buf, NULL });
  388. }
  389. void uiLog(struct Tag tag, enum UIHeat heat, const wchar_t *str) {
  390. struct Window *win = windowFor(tag);
  391. int lines = 1;
  392. waddch(win->log, '\n');
  393. if (win->mark && heat > UICold) {
  394. if (!win->unread++) {
  395. lines++;
  396. waddch(win->log, '\n');
  397. }
  398. if (heat > UIWarm) {
  399. win->hot = true;
  400. notify(tag, str);
  401. }
  402. uiStatus();
  403. }
  404. lines += addWrap(win->log, str);
  405. if (win->scroll != LogLines) win->scroll -= lines;
  406. }
  407. void uiFmt(struct Tag tag, enum UIHeat heat, const wchar_t *format, ...) {
  408. wchar_t *str;
  409. va_list ap;
  410. va_start(ap, format);
  411. vaswprintf(&str, format, ap);
  412. va_end(ap);
  413. if (!str) err(EX_OSERR, "vaswprintf");
  414. uiLog(tag, heat, str);
  415. free(str);
  416. }
  417. static void keyCode(wchar_t code) {
  418. if (code == KEY_RESIZE) uiResize();
  419. struct Window *win = windows.active;
  420. if (!win) return;
  421. switch (code) {
  422. break; case KEY_UP: windowScroll(win, -1);
  423. break; case KEY_DOWN: windowScroll(win, +1);
  424. break; case KEY_PPAGE: windowScroll(win, -(logHeight() - 1));
  425. break; case KEY_NPAGE: windowScroll(win, +(logHeight() - 1));
  426. break; case KEY_LEFT: edit(win->tag, EditLeft, 0);
  427. break; case KEY_RIGHT: edit(win->tag, EditRight, 0);
  428. break; case KEY_HOME: edit(win->tag, EditHome, 0);
  429. break; case KEY_END: edit(win->tag, EditEnd, 0);
  430. break; case KEY_DC: edit(win->tag, EditDelete, 0);
  431. break; case KEY_BACKSPACE: edit(win->tag, EditBackspace, 0);
  432. break; case KEY_ENTER: edit(win->tag, EditEnter, 0);
  433. break; default: return;
  434. }
  435. uiStatus();
  436. }
  437. static void keyMeta(wchar_t ch) {
  438. struct Window *win = windows.active;
  439. if (ch >= L'0' && ch <= L'9') uiShowNum(ch - L'0', false);
  440. if (ch == L'a') uiShowAuto();
  441. if (ch == L'/' && windows.other) uiShowWindow(windows.other);
  442. if (!win) return;
  443. switch (ch) {
  444. break; case L'b': edit(win->tag, EditBackWord, 0);
  445. break; case L'f': edit(win->tag, EditForeWord, 0);
  446. break; case L'\b': edit(win->tag, EditKillBackWord, 0);
  447. break; case L'd': edit(win->tag, EditKillForeWord, 0);
  448. break; case L'l': uiHide(); logList(win->tag);
  449. break; case L'm': uiLog(win->tag, UICold, L"");
  450. }
  451. }
  452. static void keyChar(wchar_t ch) {
  453. struct Window *win = windows.active;
  454. if (ch == CTRL(L'L')) clearok(curscr, true);
  455. if (!win) return;
  456. switch (ch) {
  457. break; case CTRL(L'N'): uiShowNum(+1, true);
  458. break; case CTRL(L'P'): uiShowNum(-1, true);
  459. break; case CTRL(L'A'): edit(win->tag, EditHome, 0);
  460. break; case CTRL(L'B'): edit(win->tag, EditLeft, 0);
  461. break; case CTRL(L'D'): edit(win->tag, EditDelete, 0);
  462. break; case CTRL(L'E'): edit(win->tag, EditEnd, 0);
  463. break; case CTRL(L'F'): edit(win->tag, EditRight, 0);
  464. break; case CTRL(L'K'): edit(win->tag, EditKillLine, 0);
  465. break; case CTRL(L'W'): edit(win->tag, EditKillBackWord, 0);
  466. break; case CTRL(L'C'): edit(win->tag, EditInsert, IRCColor);
  467. break; case CTRL(L'O'): edit(win->tag, EditInsert, IRCBold);
  468. break; case CTRL(L'R'): edit(win->tag, EditInsert, IRCColor);
  469. break; case CTRL(L'S'): edit(win->tag, EditInsert, IRCReset);
  470. break; case CTRL(L'T'): edit(win->tag, EditInsert, IRCItalic);
  471. break; case CTRL(L'U'): edit(win->tag, EditInsert, IRCUnderline);
  472. break; case CTRL(L'V'): edit(win->tag, EditInsert, IRCReverse);
  473. break; case L'\b': edit(win->tag, EditBackspace, 0);
  474. break; case L'\t': edit(win->tag, EditComplete, 0);
  475. break; case L'\n': edit(win->tag, EditEnter, 0);
  476. break; default: if (iswprint(ch)) edit(win->tag, EditInsert, ch);
  477. }
  478. }
  479. void uiRead(void) {
  480. if (ui.hide) uiShow();
  481. static bool meta;
  482. int ret;
  483. wint_t ch;
  484. enum TermEvent event;
  485. while (ERR != (ret = wget_wch(ui.input, &ch))) {
  486. if (ret == KEY_CODE_YES) {
  487. keyCode(ch);
  488. } else if (ch < 0200 && (event = termEvent((char)ch))) {
  489. struct Window *win = windows.active;
  490. switch (event) {
  491. break; case TermFocusIn: if (win) windowUnmark(win);
  492. break; case TermFocusOut: if (win) windowMark(win);
  493. break; default: {}
  494. }
  495. uiStatus();
  496. } else if (ch == Esc) {
  497. meta = true;
  498. continue;
  499. } else if (meta) {
  500. keyMeta(ch == Del ? '\b' : ch);
  501. } else {
  502. keyChar(ch == Del ? '\b' : ch);
  503. }
  504. meta = false;
  505. }
  506. uiPrompt(false);
  507. }
  508. static bool isAction(struct Tag tag, const wchar_t *input) {
  509. if (tag.id == TagStatus.id || tag.id == TagRaw.id) return false;
  510. return !wcsncasecmp(input, L"/me ", 4);
  511. }
  512. static bool isCommand(struct Tag tag, const wchar_t *input) {
  513. if (tag.id == TagStatus.id || tag.id == TagRaw.id) return true;
  514. if (input[0] != L'/') return false;
  515. const wchar_t *space = wcschr(&input[1], L' ');
  516. const wchar_t *extra = wcschr(&input[1], L'/');
  517. return !extra || (space && extra > space);
  518. }
  519. void uiPrompt(bool nickChanged) {
  520. static wchar_t *promptMesg;
  521. static wchar_t *promptAction;
  522. if (nickChanged || !promptMesg || !promptAction) {
  523. free(promptMesg);
  524. free(promptAction);
  525. enum IRCColor color = colorGen(self.user);
  526. int len = aswprintf(&promptMesg, L"\3%d<%s>\3 ", color, self.nick);
  527. if (len < 0) err(EX_OSERR, "aswprintf");
  528. len = aswprintf(&promptAction, L"\3%d* %s\3 ", color, self.nick);
  529. if (len < 0) err(EX_OSERR, "aswprintf");
  530. }
  531. const wchar_t *input = editHead();
  532. wmove(ui.input, 0, 0);
  533. if (windows.active) {
  534. if (isAction(windows.active->tag, input) && editTail() >= &input[4]) {
  535. input = &input[4];
  536. addWrap(ui.input, promptAction);
  537. } else if (!isCommand(windows.active->tag, input)) {
  538. addWrap(ui.input, promptMesg);
  539. }
  540. }
  541. int x = 0;
  542. struct Format format = { .str = input };
  543. formatReset(&format);
  544. while (formatParse(&format, editTail())) {
  545. if (format.split) getyx(ui.input, _, x);
  546. addFormat(ui.input, &format);
  547. }
  548. wclrtoeol(ui.input);
  549. wmove(ui.input, 0, x);
  550. }