1 /* Copyright (C) 2020 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 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 General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <https://www.gnu.org/licenses/>.
15 *
16 * Additional permission under GNU GPL version 3 section 7:
17 *
18 * If you modify this Program, or any covered work, by linking or
19 * combining it with OpenSSL (or a modified version of that library),
20 * containing parts covered by the terms of the OpenSSL License and the
21 * original SSLeay license, the licensors of this Program grant you
22 * additional permission to convey the resulting work. Corresponding
23 * Source for a non-source form of such a combination shall include the
24 * source code for the parts of OpenSSL used as well as that of the
25 * covered work.
26 */
27
28 #define _XOPEN_SOURCE_EXTENDED
29
30 #include <assert.h>
31 #include <ctype.h>
32 #include <curses.h>
33 #include <err.h>
34 #include <errno.h>
35 #include <fcntl.h>
36 #include <limits.h>
37 #include <signal.h>
38 #include <stdarg.h>
39 #include <stdbool.h>
40 #include <stdio.h>
41 #include <stdlib.h>
42 #include <string.h>
43 #include <sysexits.h>
44 #include <sys/file.h>
45 #include <term.h>
46 #include <termios.h>
47 #include <time.h>
48 #include <unistd.h>
49 #include <wchar.h>
50 #include <wctype.h>
51
52 #ifdef __FreeBSD__
53 #include <capsicum_helpers.h>
54 #endif
55
56 #include "chat.h"
57
58 // Annoying stuff from <term.h>:
59 #undef lines
60 #undef tab
61
62 enum {
63 StatusLines = 1,
64 MarkerLines = 1,
65 SplitLines = 5,
66 InputLines = 1,
67 InputCols = 1024,
68 };
69
70 #define BOTTOM (LINES - 1)
71 #define RIGHT (COLS - 1)
72 #define MAIN_LINES (LINES - StatusLines - InputLines)
73
74 static WINDOW *status;
75 static WINDOW *main;
76 static WINDOW *input;
77
78 struct Window {
79 uint id;
80 int scroll;
81 bool mark;
82 bool mute;
83 bool time;
84 enum Heat thresh;
85 enum Heat heat;
86 uint unreadSoft;
87 uint unreadHard;
88 uint unreadWarm;
89 struct Buffer *buffer;
90 };
91
92 static struct {
93 struct Window *ptrs[IDCap];
94 uint len;
95 uint show;
96 uint swap;
97 uint user;
98 } windows;
99
windowPush(struct Window * window)100 static uint windowPush(struct Window *window) {
101 assert(windows.len < IDCap);
102 windows.ptrs[windows.len] = window;
103 return windows.len++;
104 }
105
windowInsert(uint num,struct Window * window)106 static uint windowInsert(uint num, struct Window *window) {
107 assert(windows.len < IDCap);
108 assert(num <= windows.len);
109 memmove(
110 &windows.ptrs[num + 1],
111 &windows.ptrs[num],
112 sizeof(*windows.ptrs) * (windows.len - num)
113 );
114 windows.ptrs[num] = window;
115 windows.len++;
116 return num;
117 }
118
windowRemove(uint num)119 static struct Window *windowRemove(uint num) {
120 assert(num < windows.len);
121 struct Window *window = windows.ptrs[num];
122 windows.len--;
123 memmove(
124 &windows.ptrs[num],
125 &windows.ptrs[num + 1],
126 sizeof(*windows.ptrs) * (windows.len - num)
127 );
128 return window;
129 }
130
131 enum Heat uiThreshold = Cold;
132
windowFor(uint id)133 static uint windowFor(uint id) {
134 for (uint num = 0; num < windows.len; ++num) {
135 if (windows.ptrs[num]->id == id) return num;
136 }
137 struct Window *window = calloc(1, sizeof(*window));
138 if (!window) err(EX_OSERR, "malloc");
139 window->id = id;
140 window->mark = true;
141 window->time = uiTime.enable;
142 if (id == Network || id == Debug) {
143 window->thresh = Cold;
144 } else {
145 window->thresh = uiThreshold;
146 }
147 window->buffer = bufferAlloc();
148 completeAdd(None, idNames[id], idColors[id]);
149 return windowPush(window);
150 }
151
windowFree(struct Window * window)152 static void windowFree(struct Window *window) {
153 completeRemove(None, idNames[window->id]);
154 bufferFree(window->buffer);
155 free(window);
156 }
157
158 static short colorPairs;
159
colorInit(void)160 static void colorInit(void) {
161 start_color();
162 use_default_colors();
163 if (!COLORS) return;
164 for (short pair = 0; pair < 16; ++pair) {
165 init_pair(1 + pair, pair % COLORS, -1);
166 }
167 colorPairs = 17;
168 }
169
colorAttr(short fg)170 static attr_t colorAttr(short fg) {
171 if (!COLORS) return (fg > 0 ? A_BOLD : A_NORMAL);
172 if (fg != COLOR_BLACK && fg % COLORS == COLOR_BLACK) return A_BOLD;
173 if (COLORS > 8) return A_NORMAL;
174 return (fg / COLORS & 1 ? A_BOLD : A_NORMAL);
175 }
176
colorPair(short fg,short bg)177 static short colorPair(short fg, short bg) {
178 if (!COLORS) return 0;
179 fg %= COLORS;
180 bg %= COLORS;
181 if (bg == -1 && fg < 16) return 1 + fg;
182 for (short pair = 17; pair < colorPairs; ++pair) {
183 short f, b;
184 pair_content(pair, &f, &b);
185 if (f == fg && b == bg) return pair;
186 }
187 init_pair(colorPairs, fg, bg);
188 return colorPairs++;
189 }
190
191 #define ENUM_KEY \
192 X(KeyCtrlLeft, "\33[1;5D", NULL) \
193 X(KeyCtrlRight, "\33[1;5C", NULL) \
194 X(KeyMeta0, "\0330", "\33)") \
195 X(KeyMeta1, "\0331", "\33!") \
196 X(KeyMeta2, "\0332", "\33@") \
197 X(KeyMeta3, "\0333", "\33#") \
198 X(KeyMeta4, "\0334", "\33$") \
199 X(KeyMeta5, "\0335", "\33%") \
200 X(KeyMeta6, "\0336", "\33^") \
201 X(KeyMeta7, "\0337", "\33&") \
202 X(KeyMeta8, "\0338", "\33*") \
203 X(KeyMeta9, "\0339", "\33(") \
204 X(KeyMetaA, "\33a", NULL) \
205 X(KeyMetaB, "\33b", NULL) \
206 X(KeyMetaD, "\33d", NULL) \
207 X(KeyMetaF, "\33f", NULL) \
208 X(KeyMetaL, "\33l", NULL) \
209 X(KeyMetaM, "\33m", NULL) \
210 X(KeyMetaN, "\33n", NULL) \
211 X(KeyMetaP, "\33p", NULL) \
212 X(KeyMetaQ, "\33q", NULL) \
213 X(KeyMetaT, "\33t", NULL) \
214 X(KeyMetaU, "\33u", NULL) \
215 X(KeyMetaV, "\33v", NULL) \
216 X(KeyMetaEnter, "\33\r", "\33\n") \
217 X(KeyMetaGt, "\33>", "\33.") \
218 X(KeyMetaLt, "\33<", "\33,") \
219 X(KeyMetaEqual, "\33=", NULL) \
220 X(KeyMetaMinus, "\33-", "\33_") \
221 X(KeyMetaPlus, "\33+", NULL) \
222 X(KeyMetaSlash, "\33/", "\33?") \
223 X(KeyFocusIn, "\33[I", NULL) \
224 X(KeyFocusOut, "\33[O", NULL) \
225 X(KeyPasteOn, "\33[200~", NULL) \
226 X(KeyPasteOff, "\33[201~", NULL) \
227 X(KeyPasteManual, "\32p", "\32\20")
228
229 enum {
230 KeyMax = KEY_MAX,
231 #define X(id, seq, alt) id,
232 ENUM_KEY
233 #undef X
234 };
235
236 // XXX: Assuming terminals will be fine with these even if they're unsupported,
237 // since they're "private" modes.
238 static const char *FocusMode[2] = { "\33[?1004l", "\33[?1004h" };
239 static const char *PasteMode[2] = { "\33[?2004l", "\33[?2004h" };
240
241 struct Time uiTime = { .format = "%X" };
242
errExit(void)243 static void errExit(void) {
244 putp(FocusMode[false]);
245 putp(PasteMode[false]);
246 reset_shell_mode();
247 }
248
uiInitEarly(void)249 void uiInitEarly(void) {
250 initscr();
251 cbreak();
252 noecho();
253 colorInit();
254 atexit(errExit);
255
256 #ifndef A_ITALIC
257 #define A_ITALIC A_BLINK
258 // Force ncurses to use individual enter_attr_mode strings:
259 set_attributes = NULL;
260 enter_blink_mode = enter_italics_mode;
261 #endif
262
263 if (!to_status_line && !strncmp(termname(), "xterm", 5)) {
264 to_status_line = "\33]2;";
265 from_status_line = "\7";
266 }
267
268 #define X(id, seq, alt) define_key(seq, id); if (alt) define_key(alt, id);
269 ENUM_KEY
270 #undef X
271
272 status = newwin(StatusLines, COLS, 0, 0);
273 if (!status) err(EX_OSERR, "newwin");
274
275 main = newwin(MAIN_LINES, COLS, StatusLines, 0);
276 if (!main) err(EX_OSERR, "newwin");
277
278 int y;
279 char fmt[TimeCap];
280 char buf[TimeCap];
281 styleStrip(fmt, sizeof(fmt), uiTime.format);
282 struct tm *time = localtime(&(time_t) { -22100400 });
283 size_t len = strftime(buf, sizeof(buf), fmt, time);
284 if (!len) errx(EX_CONFIG, "invalid timestamp format: %s", fmt);
285 waddstr(main, buf);
286 waddch(main, ' ');
287 getyx(main, y, uiTime.width);
288 (void)y;
289
290 input = newpad(InputLines, InputCols);
291 if (!input) err(EX_OSERR, "newpad");
292 keypad(input, true);
293 nodelay(input, true);
294
295 windowFor(Network);
296 uiShow();
297 }
298
299 // Avoid disabling VINTR until main loop.
uiInitLate(void)300 void uiInitLate(void) {
301 struct termios term;
302 int error = tcgetattr(STDOUT_FILENO, &term);
303 if (error) err(EX_OSERR, "tcgetattr");
304
305 // Gain use of C-q, C-s, C-c, C-z, C-y, C-v, C-o.
306 term.c_iflag &= ~IXON;
307 term.c_cc[VINTR] = _POSIX_VDISABLE;
308 term.c_cc[VSUSP] = _POSIX_VDISABLE;
309 #ifdef VDSUSP
310 term.c_cc[VDSUSP] = _POSIX_VDISABLE;
311 #endif
312 term.c_cc[VLNEXT] = _POSIX_VDISABLE;
313 term.c_cc[VDISCARD] = _POSIX_VDISABLE;
314
315 error = tcsetattr(STDOUT_FILENO, TCSANOW, &term);
316 if (error) err(EX_OSERR, "tcsetattr");
317
318 def_prog_mode();
319 }
320
321 static bool hidden = true;
322 static bool waiting;
323
324 static char title[256];
325 static char prevTitle[sizeof(title)];
326
uiDraw(void)327 void uiDraw(void) {
328 if (hidden) return;
329 wnoutrefresh(status);
330 wnoutrefresh(main);
331 int y, x;
332 getyx(input, y, x);
333 pnoutrefresh(
334 input,
335 0, (x + 1 > RIGHT ? x + 1 - RIGHT : 0),
336 LINES - InputLines, 0,
337 BOTTOM, RIGHT
338 );
339 (void)y;
340 doupdate();
341
342 if (!to_status_line) return;
343 if (!strcmp(title, prevTitle)) return;
344 strcpy(prevTitle, title);
345 putp(to_status_line);
346 putp(title);
347 putp(from_status_line);
348 fflush(stdout);
349 }
350
351 static const short Colors[ColorCap] = {
352 [Default] = -1,
353 [White] = 8 + COLOR_WHITE,
354 [Black] = 0 + COLOR_BLACK,
355 [Blue] = 0 + COLOR_BLUE,
356 [Green] = 0 + COLOR_GREEN,
357 [Red] = 8 + COLOR_RED,
358 [Brown] = 0 + COLOR_RED,
359 [Magenta] = 0 + COLOR_MAGENTA,
360 [Orange] = 0 + COLOR_YELLOW,
361 [Yellow] = 8 + COLOR_YELLOW,
362 [LightGreen] = 8 + COLOR_GREEN,
363 [Cyan] = 0 + COLOR_CYAN,
364 [LightCyan] = 8 + COLOR_CYAN,
365 [LightBlue] = 8 + COLOR_BLUE,
366 [Pink] = 8 + COLOR_MAGENTA,
367 [Gray] = 8 + COLOR_BLACK,
368 [LightGray] = 0 + COLOR_WHITE,
369 52, 94, 100, 58, 22, 29, 23, 24, 17, 54, 53, 89,
370 88, 130, 142, 64, 28, 35, 30, 25, 18, 91, 90, 125,
371 124, 166, 184, 106, 34, 49, 37, 33, 19, 129, 127, 161,
372 196, 208, 226, 154, 46, 86, 51, 75, 21, 171, 201, 198,
373 203, 215, 227, 191, 83, 122, 87, 111, 63, 177, 207, 205,
374 217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212,
375 16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231,
376 };
377
styleAttr(struct Style style)378 static attr_t styleAttr(struct Style style) {
379 attr_t attr = A_NORMAL;
380 if (style.attr & Bold) attr |= A_BOLD;
381 if (style.attr & Reverse) attr |= A_REVERSE;
382 if (style.attr & Italic) attr |= A_ITALIC;
383 if (style.attr & Underline) attr |= A_UNDERLINE;
384 return attr | colorAttr(Colors[style.fg]);
385 }
386
stylePair(struct Style style)387 static short stylePair(struct Style style) {
388 return colorPair(Colors[style.fg], Colors[style.bg]);
389 }
390
styleAdd(WINDOW * win,struct Style init,const char * str)391 static int styleAdd(WINDOW *win, struct Style init, const char *str) {
392 struct Style style = init;
393 while (*str) {
394 size_t len = styleParse(&style, &str);
395 wattr_set(win, styleAttr(style), stylePair(style), NULL);
396 if (waddnstr(win, str, len) == ERR)
397 return -1;
398 str += len;
399 }
400 return 0;
401 }
402
statusUpdate(void)403 static void statusUpdate(void) {
404 struct {
405 uint unread;
406 enum Heat heat;
407 } others = { 0, Cold };
408
409 wmove(status, 0, 0);
410 for (uint num = 0; num < windows.len; ++num) {
411 const struct Window *window = windows.ptrs[num];
412 if (num != windows.show && !window->scroll) {
413 if (window->heat < Warm) continue;
414 if (window->mute && window->heat < Hot) continue;
415 }
416 if (num != windows.show) {
417 others.unread += window->unreadWarm;
418 if (window->heat > others.heat) others.heat = window->heat;
419 }
420 char buf[256], *end = &buf[sizeof(buf)];
421 char *ptr = seprintf(
422 buf, end, "\3%d%s %u%s%s %s ",
423 idColors[window->id], (num == windows.show ? "\26" : ""),
424 num, window->thresh[(const char *[]) { "-", "", "+", "++" }],
425 &"="[!window->mute], idNames[window->id]
426 );
427 if (window->mark && window->unreadWarm) {
428 ptr = seprintf(
429 ptr, end, "\3%d+%d\3%d%s",
430 (window->heat > Warm ? White : idColors[window->id]),
431 window->unreadWarm, idColors[window->id],
432 (window->scroll ? "" : " ")
433 );
434 }
435 if (window->scroll) {
436 ptr = seprintf(ptr, end, "~%d ", window->scroll);
437 }
438 if (styleAdd(status, StyleDefault, buf) < 0) break;
439 }
440 wclrtoeol(status);
441
442 const struct Window *window = windows.ptrs[windows.show];
443 char *end = &title[sizeof(title)];
444 char *ptr = seprintf(
445 title, end, "%s %s", network.name, idNames[window->id]
446 );
447 if (window->mark && window->unreadWarm) {
448 ptr = seprintf(
449 ptr, end, " +%d%s", window->unreadWarm, &"!"[window->heat < Hot]
450 );
451 }
452 if (others.unread) {
453 ptr = seprintf(
454 ptr, end, " (+%d%s)", others.unread, &"!"[others.heat < Hot]
455 );
456 }
457 }
458
mark(struct Window * window)459 static void mark(struct Window *window) {
460 if (window->scroll) return;
461 window->mark = true;
462 window->unreadSoft = 0;
463 window->unreadWarm = 0;
464 }
465
unmark(struct Window * window)466 static void unmark(struct Window *window) {
467 if (!window->scroll) {
468 window->mark = false;
469 window->heat = Cold;
470 }
471 statusUpdate();
472 }
473
uiShow(void)474 void uiShow(void) {
475 if (!hidden) return;
476 prevTitle[0] = '\0';
477 putp(FocusMode[true]);
478 putp(PasteMode[true]);
479 fflush(stdout);
480 hidden = false;
481 unmark(windows.ptrs[windows.show]);
482 }
483
uiHide(void)484 void uiHide(void) {
485 if (hidden) return;
486 mark(windows.ptrs[windows.show]);
487 hidden = true;
488 putp(FocusMode[false]);
489 putp(PasteMode[false]);
490 endwin();
491 }
492
windowTop(const struct Window * window)493 static size_t windowTop(const struct Window *window) {
494 size_t top = BufferCap - MAIN_LINES - window->scroll;
495 if (window->scroll) top += MarkerLines;
496 return top;
497 }
498
windowBottom(const struct Window * window)499 static size_t windowBottom(const struct Window *window) {
500 size_t bottom = BufferCap - (window->scroll ?: 1);
501 if (window->scroll) bottom -= SplitLines + MarkerLines;
502 return bottom;
503 }
504
windowCols(const struct Window * window)505 static int windowCols(const struct Window *window) {
506 return COLS - (window->time ? uiTime.width : 0);
507 }
508
mainAdd(int y,bool time,const struct Line * line)509 static void mainAdd(int y, bool time, const struct Line *line) {
510 int ny, nx;
511 wmove(main, y, 0);
512 if (!line || !line->str[0]) {
513 wclrtoeol(main);
514 return;
515 }
516 if (time && line->time) {
517 char buf[TimeCap];
518 strftime(buf, sizeof(buf), uiTime.format, localtime(&line->time));
519 struct Style init = { .fg = Gray, .bg = Default };
520 styleAdd(main, init, buf);
521 waddch(main, ' ');
522 } else if (time) {
523 whline(main, ' ', uiTime.width);
524 wmove(main, y, uiTime.width);
525 }
526 styleAdd(main, StyleDefault, line->str);
527 getyx(main, ny, nx);
528 if (ny != y) return;
529 wclrtoeol(main);
530 (void)nx;
531 }
532
mainUpdate(void)533 static void mainUpdate(void) {
534 struct Window *window = windows.ptrs[windows.show];
535
536 int y = 0;
537 int marker = MAIN_LINES - SplitLines - MarkerLines;
538 for (size_t i = windowTop(window); i < BufferCap; ++i) {
539 mainAdd(y++, window->time, bufferHard(window->buffer, i));
540 if (window->scroll && y == marker) break;
541 }
542 if (!window->scroll) return;
543
544 y = MAIN_LINES - SplitLines;
545 for (size_t i = BufferCap - SplitLines; i < BufferCap; ++i) {
546 mainAdd(y++, window->time, bufferHard(window->buffer, i));
547 }
548 wattr_set(main, A_NORMAL, 0, NULL);
549 mvwhline(main, marker, 0, ACS_BULLET, COLS);
550 }
551
windowScroll(struct Window * window,int n)552 static void windowScroll(struct Window *window, int n) {
553 mark(window);
554 window->scroll += n;
555 if (window->scroll > BufferCap - MAIN_LINES) {
556 window->scroll = BufferCap - MAIN_LINES;
557 }
558 if (window->scroll < 0) window->scroll = 0;
559 unmark(window);
560 if (window == windows.ptrs[windows.show]) mainUpdate();
561 }
562
563 struct Util uiNotifyUtil;
notify(uint id,const char * str)564 static void notify(uint id, const char *str) {
565 if (self.restricted) return;
566 if (!uiNotifyUtil.argc) return;
567
568 char buf[1024];
569 styleStrip(buf, sizeof(buf), str);
570
571 struct Util util = uiNotifyUtil;
572 utilPush(&util, idNames[id]);
573 utilPush(&util, buf);
574
575 pid_t pid = fork();
576 if (pid < 0) err(EX_OSERR, "fork");
577 if (pid) return;
578
579 setsid();
580 close(STDIN_FILENO);
581 dup2(utilPipe[1], STDOUT_FILENO);
582 dup2(utilPipe[1], STDERR_FILENO);
583 execvp(util.argv[0], (char *const *)util.argv);
584 warn("%s", util.argv[0]);
585 _exit(EX_CONFIG);
586 }
587
uiWrite(uint id,enum Heat heat,const time_t * src,const char * str)588 void uiWrite(uint id, enum Heat heat, const time_t *src, const char *str) {
589 struct Window *window = windows.ptrs[windowFor(id)];
590 time_t ts = (src ? *src : time(NULL));
591
592 if (heat >= window->thresh) {
593 if (!window->unreadSoft++) window->unreadHard = 0;
594 }
595 if (window->mark && heat > Cold) {
596 if (!window->unreadWarm++) {
597 int lines = bufferPush(
598 window->buffer, windowCols(window),
599 window->thresh, Warm, ts, ""
600 );
601 if (window->scroll) windowScroll(window, lines);
602 if (window->unreadSoft > 1) {
603 window->unreadSoft++;
604 window->unreadHard += lines;
605 }
606 }
607 if (heat > window->heat) window->heat = heat;
608 statusUpdate();
609 }
610 int lines = bufferPush(
611 window->buffer, windowCols(window),
612 window->thresh, heat, ts, str
613 );
614 window->unreadHard += lines;
615 if (window->scroll) windowScroll(window, lines);
616 if (window == windows.ptrs[windows.show]) mainUpdate();
617
618 if (window->mark && heat > Warm) {
619 beep();
620 notify(id, str);
621 }
622 }
623
uiFormat(uint id,enum Heat heat,const time_t * time,const char * format,...)624 void uiFormat(
625 uint id, enum Heat heat, const time_t *time, const char *format, ...
626 ) {
627 char buf[1024];
628 va_list ap;
629 va_start(ap, format);
630 int len = vsnprintf(buf, sizeof(buf), format, ap);
631 va_end(ap);
632 assert((size_t)len < sizeof(buf));
633 uiWrite(id, heat, time, buf);
634 }
635
scrollTo(struct Window * window,int top)636 static void scrollTo(struct Window *window, int top) {
637 window->scroll = 0;
638 windowScroll(window, top - MAIN_LINES + MarkerLines);
639 }
640
windowReflow(struct Window * window)641 static void windowReflow(struct Window *window) {
642 uint num = 0;
643 const struct Line *line = bufferHard(window->buffer, windowTop(window));
644 if (line) num = line->num;
645 window->unreadHard = bufferReflow(
646 window->buffer, windowCols(window),
647 window->thresh, window->unreadSoft
648 );
649 if (!window->scroll || !num) return;
650 for (size_t i = 0; i < BufferCap; ++i) {
651 line = bufferHard(window->buffer, i);
652 if (!line || line->num != num) continue;
653 scrollTo(window, BufferCap - i);
654 break;
655 }
656 }
657
resize(void)658 static void resize(void) {
659 wclear(main);
660 wresize(main, MAIN_LINES, COLS);
661 for (uint num = 0; num < windows.len; ++num) {
662 windowReflow(windows.ptrs[num]);
663 }
664 statusUpdate();
665 mainUpdate();
666 }
667
windowList(const struct Window * window)668 static void windowList(const struct Window *window) {
669 uiHide();
670 waiting = true;
671
672 uint num = 0;
673 const struct Line *line = bufferHard(window->buffer, windowBottom(window));
674 if (line) num = line->num;
675 for (size_t i = 0; i < BufferCap; ++i) {
676 line = bufferSoft(window->buffer, i);
677 if (!line) continue;
678 if (line->num > num) break;
679 if (!line->str[0]) {
680 printf("\n");
681 continue;
682 }
683
684 char buf[TimeCap];
685 strftime(buf, sizeof(buf), uiTime.format, localtime(&line->time));
686 vid_attr(colorAttr(Colors[Gray]), colorPair(Colors[Gray], -1), NULL);
687 printf("%s ", buf);
688
689 bool align = false;
690 struct Style style = StyleDefault;
691 for (const char *str = line->str; *str;) {
692 if (*str == '\t') {
693 printf("%c", (align ? '\t' : ' '));
694 align = true;
695 str++;
696 }
697
698 size_t len = styleParse(&style, &str);
699 size_t tab = strcspn(str, "\t");
700 if (tab < len) len = tab;
701
702 vid_attr(styleAttr(style), stylePair(style), NULL);
703 printf("%.*s", (int)len, str);
704 str += len;
705 }
706 printf("\n");
707 }
708 }
709
inputAdd(struct Style reset,struct Style * style,const char * str)710 static void inputAdd(struct Style reset, struct Style *style, const char *str) {
711 while (*str) {
712 const char *code = str;
713 size_t len = styleParse(style, &str);
714 wattr_set(input, A_BOLD | A_REVERSE, 0, NULL);
715 switch (*code) {
716 break; case B: waddch(input, 'B');
717 break; case C: waddch(input, 'C');
718 break; case O: waddch(input, 'O');
719 break; case R: waddch(input, 'R');
720 break; case I: waddch(input, 'I');
721 break; case U: waddch(input, 'U');
722 break; case '\n': waddch(input, 'N');
723 }
724 if (str - code > 1) waddnstr(input, &code[1], str - &code[1]);
725 if (str[0] == '\n') {
726 *style = reset;
727 str++;
728 len--;
729 }
730 size_t nl = strcspn(str, "\n");
731 if (nl < len) len = nl;
732 wattr_set(input, styleAttr(*style), stylePair(*style), NULL);
733 waddnstr(input, str, len);
734 str += len;
735 }
736 }
737
inputStop(struct Style reset,struct Style * style,const char * str,char * stop)738 static char *inputStop(
739 struct Style reset, struct Style *style,
740 const char *str, char *stop
741 ) {
742 char ch = *stop;
743 *stop = '\0';
744 inputAdd(reset, style, str);
745 *stop = ch;
746 return stop;
747 }
748
inputUpdate(void)749 static void inputUpdate(void) {
750 size_t pos;
751 char *buf = editBuffer(&pos);
752 struct Window *window = windows.ptrs[windows.show];
753
754 const char *prefix = "";
755 const char *prompt = self.nick;
756 const char *suffix = "";
757 const char *skip = buf;
758 struct Style stylePrompt = { .fg = self.color, .bg = Default };
759 struct Style styleInput = StyleDefault;
760
761 size_t split = commandWillSplit(window->id, buf);
762 const char *privmsg = commandIsPrivmsg(window->id, buf);
763 const char *notice = commandIsNotice(window->id, buf);
764 const char *action = commandIsAction(window->id, buf);
765 if (privmsg) {
766 prefix = "<"; suffix = "> ";
767 skip = privmsg;
768 } else if (notice) {
769 prefix = "-"; suffix = "- ";
770 styleInput.fg = LightGray;
771 skip = notice;
772 } else if (action) {
773 prefix = "* "; suffix = " ";
774 stylePrompt.attr |= Italic;
775 styleInput.attr |= Italic;
776 skip = action;
777 } else if (window->id == Debug && buf[0] != '/') {
778 prompt = "<< ";
779 stylePrompt.fg = Gray;
780 } else {
781 prompt = "";
782 }
783 if (skip > &buf[pos]) {
784 prefix = prompt = suffix = "";
785 skip = buf;
786 }
787
788 wmove(input, 0, 0);
789 if (window->time && window->id != Network) {
790 whline(input, ' ', uiTime.width);
791 wmove(input, 0, uiTime.width);
792 }
793 wattr_set(input, styleAttr(stylePrompt), stylePair(stylePrompt), NULL);
794 waddstr(input, prefix);
795 waddstr(input, prompt);
796 waddstr(input, suffix);
797
798 int y, x;
799 const char *ptr = skip;
800 struct Style style = styleInput;
801 if (split && split < pos) {
802 ptr = inputStop(styleInput, &style, ptr, &buf[split]);
803 style = styleInput;
804 style.bg = Red;
805 }
806 ptr = inputStop(styleInput, &style, ptr, &buf[pos]);
807 getyx(input, y, x);
808 if (split && split >= pos) {
809 ptr = inputStop(styleInput, &style, ptr, &buf[split]);
810 style = styleInput;
811 style.bg = Red;
812 }
813 inputAdd(styleInput, &style, ptr);
814 wclrtoeol(input);
815 wmove(input, y, x);
816 }
817
uiWindows(void)818 void uiWindows(void) {
819 for (uint num = 0; num < windows.len; ++num) {
820 const struct Window *window = windows.ptrs[num];
821 uiFormat(
822 Network, Warm, NULL, "\3%02d%u %s",
823 idColors[window->id], num, idNames[window->id]
824 );
825 }
826 }
827
windowShow(uint num)828 static void windowShow(uint num) {
829 if (num != windows.show) {
830 windows.swap = windows.show;
831 mark(windows.ptrs[windows.swap]);
832 }
833 windows.show = num;
834 windows.user = num;
835 unmark(windows.ptrs[windows.show]);
836 mainUpdate();
837 inputUpdate();
838 }
839
uiShowID(uint id)840 void uiShowID(uint id) {
841 windowShow(windowFor(id));
842 }
843
uiShowNum(uint num)844 void uiShowNum(uint num) {
845 if (num < windows.len) windowShow(num);
846 }
847
uiMoveID(uint id,uint num)848 void uiMoveID(uint id, uint num) {
849 struct Window *window = windowRemove(windowFor(id));
850 if (num < windows.len) {
851 windowShow(windowInsert(num, window));
852 } else {
853 windowShow(windowPush(window));
854 }
855 }
856
windowClose(uint num)857 static void windowClose(uint num) {
858 if (windows.ptrs[num]->id == Network) return;
859 struct Window *window = windowRemove(num);
860 completeClear(window->id);
861 windowFree(window);
862 if (windows.swap >= num) windows.swap--;
863 if (windows.show == num) {
864 windowShow(windows.swap);
865 windows.swap = windows.show;
866 } else if (windows.show > num) {
867 windows.show--;
868 mainUpdate();
869 }
870 statusUpdate();
871 }
872
uiCloseID(uint id)873 void uiCloseID(uint id) {
874 windowClose(windowFor(id));
875 }
876
uiCloseNum(uint num)877 void uiCloseNum(uint num) {
878 if (num < windows.len) windowClose(num);
879 }
880
scrollPage(struct Window * window,int n)881 static void scrollPage(struct Window *window, int n) {
882 windowScroll(window, n * (MAIN_LINES - SplitLines - MarkerLines - 1));
883 }
884
scrollTop(struct Window * window)885 static void scrollTop(struct Window *window) {
886 for (size_t i = 0; i < BufferCap; ++i) {
887 if (!bufferHard(window->buffer, i)) continue;
888 scrollTo(window, BufferCap - i);
889 break;
890 }
891 }
892
scrollHot(struct Window * window,int dir)893 static void scrollHot(struct Window *window, int dir) {
894 for (size_t i = windowTop(window) + dir; i < BufferCap; i += dir) {
895 const struct Line *line = bufferHard(window->buffer, i);
896 const struct Line *prev = bufferHard(window->buffer, i - 1);
897 if (!line || line->heat < Hot) continue;
898 if (prev && prev->heat > Warm) continue;
899 scrollTo(window, BufferCap - i);
900 break;
901 }
902 }
903
scrollSearch(struct Window * window,const char * str,int dir)904 static void scrollSearch(struct Window *window, const char *str, int dir) {
905 for (size_t i = windowTop(window) + dir; i < BufferCap; i += dir) {
906 const struct Line *line = bufferHard(window->buffer, i);
907 if (!line || !strcasestr(line->str, str)) continue;
908 scrollTo(window, BufferCap - i);
909 break;
910 }
911 }
912
toggleTime(struct Window * window)913 static void toggleTime(struct Window *window) {
914 window->time ^= true;
915 windowReflow(window);
916 statusUpdate();
917 mainUpdate();
918 inputUpdate();
919 }
920
incThresh(struct Window * window,int n)921 static void incThresh(struct Window *window, int n) {
922 if (n > 0 && window->thresh == Hot) return;
923 if (n < 0 && window->thresh == Ice) {
924 window->thresh = Cold;
925 } else {
926 window->thresh += n;
927 }
928 windowReflow(window);
929 statusUpdate();
930 mainUpdate();
931 statusUpdate();
932 }
933
showAuto(void)934 static void showAuto(void) {
935 uint minHot = UINT_MAX, numHot = 0;
936 uint minWarm = UINT_MAX, numWarm = 0;
937 for (uint num = 0; num < windows.len; ++num) {
938 struct Window *window = windows.ptrs[num];
939 if (window->heat >= Hot) {
940 if (window->unreadWarm >= minHot) continue;
941 minHot = window->unreadWarm;
942 numHot = num;
943 }
944 if (window->heat >= Warm && !window->mute) {
945 if (window->unreadWarm >= minWarm) continue;
946 minWarm = window->unreadWarm;
947 numWarm = num;
948 }
949 }
950 uint user = windows.user;
951 if (minHot < UINT_MAX) {
952 windowShow(numHot);
953 windows.user = user;
954 } else if (minWarm < UINT_MAX) {
955 windowShow(numWarm);
956 windows.user = user;
957 } else if (user != windows.show) {
958 windowShow(user);
959 }
960 }
961
keyCode(int code)962 static void keyCode(int code) {
963 struct Window *window = windows.ptrs[windows.show];
964 uint id = window->id;
965 switch (code) {
966 break; case KEY_RESIZE: resize();
967 break; case KeyFocusIn: unmark(window);
968 break; case KeyFocusOut: mark(window);
969
970 break; case KeyMetaEnter: edit(id, EditInsert, L'\n');
971 break; case KeyMetaEqual: window->mute ^= true; statusUpdate();
972 break; case KeyMetaMinus: incThresh(window, -1);
973 break; case KeyMetaPlus: incThresh(window, +1);
974 break; case KeyMetaSlash: windowShow(windows.swap);
975
976 break; case KeyMetaGt: scrollTo(window, 0);
977 break; case KeyMetaLt: scrollTop(window);
978
979 break; case KeyMeta0 ... KeyMeta9: uiShowNum(code - KeyMeta0);
980 break; case KeyMetaA: showAuto();
981 break; case KeyMetaB: edit(id, EditPrevWord, 0);
982 break; case KeyMetaD: edit(id, EditDeleteNextWord, 0);
983 break; case KeyMetaF: edit(id, EditNextWord, 0);
984 break; case KeyMetaL: windowList(window);
985 break; case KeyMetaM: uiWrite(id, Warm, NULL, "");
986 break; case KeyMetaN: scrollHot(window, +1);
987 break; case KeyMetaP: scrollHot(window, -1);
988 break; case KeyMetaQ: edit(id, EditCollapse, 0);
989 break; case KeyMetaT: toggleTime(window);
990 break; case KeyMetaU: scrollTo(window, window->unreadHard);
991 break; case KeyMetaV: scrollPage(window, +1);
992
993 break; case KeyCtrlLeft: edit(id, EditPrevWord, 0);
994 break; case KeyCtrlRight: edit(id, EditNextWord, 0);
995
996 break; case KEY_BACKSPACE: edit(id, EditDeletePrev, 0);
997 break; case KEY_DC: edit(id, EditDeleteNext, 0);
998 break; case KEY_DOWN: windowScroll(window, -1);
999 break; case KEY_END: edit(id, EditTail, 0);
1000 break; case KEY_ENTER: edit(id, EditEnter, 0);
1001 break; case KEY_HOME: edit(id, EditHead, 0);
1002 break; case KEY_LEFT: edit(id, EditPrev, 0);
1003 break; case KEY_NPAGE: scrollPage(window, -1);
1004 break; case KEY_PPAGE: scrollPage(window, +1);
1005 break; case KEY_RIGHT: edit(id, EditNext, 0);
1006 break; case KEY_SEND: scrollTo(window, 0);
1007 break; case KEY_SHOME: scrollTo(window, BufferCap);
1008 break; case KEY_UP: windowScroll(window, +1);
1009 }
1010 }
1011
keyCtrl(wchar_t ch)1012 static void keyCtrl(wchar_t ch) {
1013 struct Window *window = windows.ptrs[windows.show];
1014 uint id = window->id;
1015 switch (ch ^ L'@') {
1016 break; case L'?': edit(id, EditDeletePrev, 0);
1017 break; case L'A': edit(id, EditHead, 0);
1018 break; case L'B': edit(id, EditPrev, 0);
1019 break; case L'C': raise(SIGINT);
1020 break; case L'D': edit(id, EditDeleteNext, 0);
1021 break; case L'E': edit(id, EditTail, 0);
1022 break; case L'F': edit(id, EditNext, 0);
1023 break; case L'H': edit(id, EditDeletePrev, 0);
1024 break; case L'I': edit(id, EditComplete, 0);
1025 break; case L'J': edit(id, EditEnter, 0);
1026 break; case L'K': edit(id, EditDeleteTail, 0);
1027 break; case L'L': clearok(curscr, true);
1028 break; case L'N': uiShowNum(windows.show + 1);
1029 break; case L'P': uiShowNum(windows.show - 1);
1030 break; case L'R': scrollSearch(window, editBuffer(NULL), -1);
1031 break; case L'S': scrollSearch(window, editBuffer(NULL), +1);
1032 break; case L'T': edit(id, EditTranspose, 0);
1033 break; case L'U': edit(id, EditDeleteHead, 0);
1034 break; case L'V': scrollPage(window, -1);
1035 break; case L'W': edit(id, EditDeletePrevWord, 0);
1036 break; case L'X': edit(id, EditExpand, 0);
1037 break; case L'Y': edit(id, EditPaste, 0);
1038 }
1039 }
1040
keyStyle(wchar_t ch)1041 static void keyStyle(wchar_t ch) {
1042 uint id = windows.ptrs[windows.show]->id;
1043 if (iswcntrl(ch)) ch = towlower(ch ^ L'@');
1044 enum Color color = Default;
1045 switch (ch) {
1046 break; case L'A': color = Gray;
1047 break; case L'B': color = Blue;
1048 break; case L'C': color = Cyan;
1049 break; case L'G': color = Green;
1050 break; case L'K': color = Black;
1051 break; case L'M': color = Magenta;
1052 break; case L'N': color = Brown;
1053 break; case L'O': color = Orange;
1054 break; case L'P': color = Pink;
1055 break; case L'R': color = Red;
1056 break; case L'W': color = White;
1057 break; case L'Y': color = Yellow;
1058 break; case L'b': edit(id, EditInsert, B);
1059 break; case L'c': edit(id, EditInsert, C);
1060 break; case L'i': edit(id, EditInsert, I);
1061 break; case L'o': edit(id, EditInsert, O);
1062 break; case L'r': edit(id, EditInsert, R);
1063 break; case L'u': edit(id, EditInsert, U);
1064 }
1065 if (color != Default) {
1066 char buf[4];
1067 snprintf(buf, sizeof(buf), "%c%02d", C, color);
1068 for (char *ch = buf; *ch; ++ch) {
1069 edit(id, EditInsert, *ch);
1070 }
1071 }
1072 }
1073
uiRead(void)1074 void uiRead(void) {
1075 if (hidden) {
1076 if (waiting) {
1077 uiShow();
1078 flushinp();
1079 waiting = false;
1080 } else {
1081 return;
1082 }
1083 }
1084
1085 wint_t ch;
1086 static bool paste, style, literal;
1087 for (int ret; ERR != (ret = wget_wch(input, &ch));) {
1088 if (ret == KEY_CODE_YES && ch == KeyPasteOn) {
1089 paste = true;
1090 } else if (ret == KEY_CODE_YES && ch == KeyPasteOff) {
1091 paste = false;
1092 } else if (ret == KEY_CODE_YES && ch == KeyPasteManual) {
1093 paste ^= true;
1094 } else if (paste || literal) {
1095 edit(windows.ptrs[windows.show]->id, EditInsert, ch);
1096 } else if (ret == KEY_CODE_YES) {
1097 keyCode(ch);
1098 } else if (ch == (L'Z' ^ L'@')) {
1099 style = true;
1100 continue;
1101 } else if (style && ch == (L'V' ^ L'@')) {
1102 literal = true;
1103 continue;
1104 } else if (style) {
1105 keyStyle(ch);
1106 } else if (iswcntrl(ch)) {
1107 keyCtrl(ch);
1108 } else {
1109 edit(windows.ptrs[windows.show]->id, EditInsert, ch);
1110 }
1111 style = false;
1112 literal = false;
1113 }
1114 inputUpdate();
1115 }
1116
1117 static const time_t Signatures[] = {
1118 0x6C72696774616301, // no heat, unread, unreadWarm
1119 0x6C72696774616302, // no self.pos
1120 0x6C72696774616303, // no buffer line heat
1121 0x6C72696774616304, // no mute
1122 0x6C72696774616305, // no URLs
1123 0x6C72696774616306, // no thresh
1124 0x6C72696774616307, // no window time
1125 0x6C72696774616308,
1126 };
1127
signatureVersion(time_t signature)1128 static size_t signatureVersion(time_t signature) {
1129 for (size_t i = 0; i < ARRAY_LEN(Signatures); ++i) {
1130 if (signature == Signatures[i]) return i;
1131 }
1132 errx(EX_DATAERR, "unknown file signature %jX", (uintmax_t)signature);
1133 }
1134
writeTime(FILE * file,time_t time)1135 static int writeTime(FILE *file, time_t time) {
1136 return (fwrite(&time, sizeof(time), 1, file) ? 0 : -1);
1137 }
writeString(FILE * file,const char * str)1138 static int writeString(FILE *file, const char *str) {
1139 return (fwrite(str, strlen(str) + 1, 1, file) ? 0 : -1);
1140 }
1141
1142 static FILE *saveFile;
1143
uiSave(void)1144 int uiSave(void) {
1145 int error = 0
1146 || ftruncate(fileno(saveFile), 0)
1147 || writeTime(saveFile, Signatures[7])
1148 || writeTime(saveFile, self.pos);
1149 if (error) return error;
1150 for (uint num = 0; num < windows.len; ++num) {
1151 const struct Window *window = windows.ptrs[num];
1152 error = 0
1153 || writeString(saveFile, idNames[window->id])
1154 || writeTime(saveFile, window->mute)
1155 || writeTime(saveFile, window->time)
1156 || writeTime(saveFile, window->thresh)
1157 || writeTime(saveFile, window->heat)
1158 || writeTime(saveFile, window->unreadSoft)
1159 || writeTime(saveFile, window->unreadWarm);
1160 if (error) return error;
1161 for (size_t i = 0; i < BufferCap; ++i) {
1162 const struct Line *line = bufferSoft(window->buffer, i);
1163 if (!line) continue;
1164 error = 0
1165 || writeTime(saveFile, line->time)
1166 || writeTime(saveFile, line->heat)
1167 || writeString(saveFile, line->str);
1168 if (error) return error;
1169 }
1170 error = writeTime(saveFile, 0);
1171 if (error) return error;
1172 }
1173 return 0
1174 || writeString(saveFile, "")
1175 || urlSave(saveFile)
1176 || fclose(saveFile);
1177 }
1178
readTime(FILE * file)1179 static time_t readTime(FILE *file) {
1180 time_t time;
1181 fread(&time, sizeof(time), 1, file);
1182 if (ferror(file)) err(EX_IOERR, "fread");
1183 if (feof(file)) errx(EX_DATAERR, "unexpected eof");
1184 return time;
1185 }
readString(FILE * file,char ** buf,size_t * cap)1186 static ssize_t readString(FILE *file, char **buf, size_t *cap) {
1187 ssize_t len = getdelim(buf, cap, '\0', file);
1188 if (len < 0 && !feof(file)) err(EX_IOERR, "getdelim");
1189 return len;
1190 }
1191
uiLoad(const char * name)1192 void uiLoad(const char *name) {
1193 int error;
1194 saveFile = dataOpen(name, "a+e");
1195 if (!saveFile) exit(EX_CANTCREAT);
1196 rewind(saveFile);
1197
1198 #ifdef __FreeBSD__
1199 cap_rights_t rights;
1200 cap_rights_init(&rights, CAP_READ, CAP_WRITE, CAP_FLOCK, CAP_FTRUNCATE);
1201 error = caph_rights_limit(fileno(saveFile), &rights);
1202 if (error) err(EX_OSERR, "cap_rights_limit");
1203 #endif
1204
1205 error = flock(fileno(saveFile), LOCK_EX | LOCK_NB);
1206 if (error && errno == EWOULDBLOCK) {
1207 errx(EX_CANTCREAT, "%s: save file in use", name);
1208 }
1209
1210 time_t signature;
1211 fread(&signature, sizeof(signature), 1, saveFile);
1212 if (ferror(saveFile)) err(EX_IOERR, "fread");
1213 if (feof(saveFile)) {
1214 return;
1215 }
1216 size_t version = signatureVersion(signature);
1217
1218 if (version > 1) {
1219 self.pos = readTime(saveFile);
1220 }
1221
1222 char *buf = NULL;
1223 size_t cap = 0;
1224 while (0 < readString(saveFile, &buf, &cap) && buf[0]) {
1225 struct Window *window = windows.ptrs[windowFor(idFor(buf))];
1226 if (version > 3) window->mute = readTime(saveFile);
1227 if (version > 6) window->time = readTime(saveFile);
1228 if (version > 5) window->thresh = readTime(saveFile);
1229 if (version > 0) {
1230 window->heat = readTime(saveFile);
1231 window->unreadSoft = readTime(saveFile);
1232 window->unreadWarm = readTime(saveFile);
1233 }
1234 for (;;) {
1235 time_t time = readTime(saveFile);
1236 if (!time) break;
1237 enum Heat heat = (version > 2 ? readTime(saveFile) : Cold);
1238 readString(saveFile, &buf, &cap);
1239 bufferPush(window->buffer, COLS, window->thresh, heat, time, buf);
1240 }
1241 windowReflow(window);
1242 }
1243 urlLoad(saveFile, version);
1244
1245 free(buf);
1246 }
1247