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

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