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