1 module dcc.curses.curses;
2 
3 private import std.stdio;
4 private import std.string;
5 private import std.regex;
6 private import std.utf : count;
7 private import std.conv : to;
8 private import std.algorithm : sort, find;
9 private import std.file : exists, copy;
10 private import std.process : environment;
11 
12 private import core.thread : Thread;
13 private import core.time : dur, msecs, Duration;
14 
15 private import std.parallelism;
16 
17 private import dcc.engine.conf;
18 private import dcc.engine.tribune;
19 private import dcc.curses.uput;
20 
21 private import deimos.ncurses.ncurses;
22 private import deimos.ncurses.panel;
23 
24 private static import std.utf;
25 private static import std.array;
26 
27 extern (C) { char* setlocale(int category, const char* locale); }
28 
29 void main(string[] args) {
30 	setlocale(0, "".toStringz());
31 
32 	string config_file = environment.get("HOME") ~ "/.dcoincoinrc";
33 
34 	if (!config_file.exists()) {
35 		foreach (string prefix; ["/usr", "/usr/local"]) {
36 			string rc = prefix ~ "/share/doc/dcoincoin/dcoincoinrc";
37 			if (rc.exists()) {
38 				try {
39 					rc.copy(config_file);
40 					stderr.writeln("Initialized ", config_file, " with ", rc, ".");
41 					break;
42 				}
43 				catch (Exception e) {
44 					// Nothing special to do here.
45 				}
46 			}
47 		}
48 	}
49 
50 	if (args.length == 2) {
51 		config_file = args[1];
52 	}
53 
54 	if (!config_file.exists()) {
55 		stderr.writeln("Configuration file ", config_file, " does not exist.");
56 		return;
57 	}
58 
59 	NCUI ui = new NCUI(config_file);
60 
61 	if (ui.tribunes.length == 0) {
62 		endwin();
63 		stderr.writeln("You should try to configure at least one tribune!");
64 	} else {
65 		ui.loop();
66 	}
67 }
68 
69 struct Stop {
70 	int offset;
71 	int start;
72 	int length;
73 	NCPost post;
74 	attr_t attributes;
75 	short color;
76 	NCPost referenced_post;
77 }
78 
79 class NCUI {
80 	string config_file;
81 	Config config;
82 	NCTribune[string] tribunes;
83 	NCTribune[] tribunes_ordered;
84 	Duration[NCTribune] reload_remaining;
85 	ulong active = 0;
86 
87 	WINDOW* posts_window;
88 	WINDOW* input_window;
89 	WINDOW* preview_window;
90 
91 	PANEL* main_panel;
92 	PANEL* preview_panel;
93 
94 	Stop current_stop;
95 	Stop[] stops;
96 	int offset;
97 
98 	ulong[string] colors;
99 
100 	bool display_enabled = false;
101 
102 	this(string config_file) {
103 		this.config_file = config_file;
104 
105 		this.config = new Config(this.config_file);
106 
107 		this.init_ui();
108 
109 		auto n_tribunes = this.config.tribunes.length;
110 		int n = 2;
111 		// Create all tribunes, then fetch their posts without displaying
112 		// them, and once all this is done then display the latest posts.
113 		foreach (Tribune tribune ; this.config.tribunes) {
114 			this.tribunes[tribune.name] = new NCTribune(this, tribune);
115 
116 			this.tribunes[tribune.name].color = n;
117 			n++;
118 			if (n >= 7) {
119 				n = 2;
120 			}
121 		}
122 
123 		this.tribunes_ordered = this.tribunes.values;
124 
125 		foreach (ref tribune; parallel(this.tribunes.values)) {
126 			tribune.tribune.fetch_posts();
127 
128 			n_tribunes--;
129 
130 			if (n_tribunes == 0) {
131 				this.display_all_posts();
132 				this.posts_to_display.length = 0;
133 			}
134 		}
135 		
136 		this.start_timers();
137 	}
138 
139 	void reload_tick() {
140 		foreach (tribune ; this.tribunes) {
141 			if (tribune !in this.reload_remaining) {
142 				this.reload_remaining[tribune] = dur!("seconds")(tribune.tribune.refresh);
143 			}
144 
145 			this.reload_remaining[tribune] -= 10.msecs;
146 
147 			if (this.reload_remaining[tribune] <= 0.msecs) {
148 				tribune.fetch_posts();
149 				this.reload_remaining[tribune] = dur!("seconds")(tribune.tribune.refresh);
150 			}
151 		}
152 
153 		this.set_status("");
154 	}
155 
156 	void redraw_all_posts() {
157 		endwin();
158 		refresh();
159 		this.init_ui();
160 		this.display_all_posts();
161 	}
162 
163 	void display_all_posts() {
164 		this.stops.length = 0;
165 		this.current_stop = Stop.init;
166 		this.offset = 0;
167 		NCPost[] posts;
168 		foreach (NCTribune tribune; this.tribunes) {
169 			posts ~= tribune.posts;
170 		}
171 
172 		if (posts.length > 0) {
173 			posts.sort!((a, b) {
174 				if (a.post.timestamp == b.post.timestamp) {
175 					return a.post.post_id < b.post.post_id;
176 				} else {
177 					return a.post.timestamp < b.post.timestamp;
178 				}
179 			});
180 
181 			this.display_enabled = true;
182 			if (this.posts_window.maxy > 0) {
183 				auto start = this.posts_window.maxy > posts.length ? 0 : posts.length - this.posts_window.maxy;
184 				foreach (NCPost post; posts[$-this.posts_window.maxy .. $]) {
185 					this.display_post(this.posts_window, post, true, false);
186 				}
187 			}
188 
189 			set_stop(this.stops[$ - 1]);
190 		}
191 	}
192 
193 	void start_timers() {
194 		task({
195 			this.display_enabled = true;
196 			while (true) {
197 				Thread.sleep(10.msecs);
198 				this.reload_tick();
199 			}
200 		}).executeInNewThread();
201 	}
202 
203 	void init_ui() {
204 		initscr();
205 		start_color();
206 		this.init_colors();
207 
208 		curs_set(0);
209 
210 		int input_height = 2;
211 
212 		this.posts_window = newwin(LINES - input_height, 0, 0, 0);
213 		this.input_window = newwin(2, 0, LINES - input_height, 0);
214 
215 		this.preview_window = newwin(4, 0, 0, 0);
216 
217 		this.main_panel = new_panel(posts_window);
218 		this.preview_panel = new_panel(preview_window);
219 
220 		top_panel(this.main_panel);
221 		update_panels();
222 		doupdate();
223 
224 		mvwhline(this.input_window, 0, 0, 0, COLS);
225 
226 		wrefresh(this.posts_window);
227 		wrefresh(this.input_window);
228 
229 		scrollok(this.posts_window, true);
230 	}
231 
232 	void init_colors() {
233 		init_pair( 1, COLOR_WHITE,   COLOR_BLACK);
234 		init_pair( 2, COLOR_RED,     COLOR_BLACK);
235 		init_pair( 3, COLOR_GREEN,   COLOR_BLACK);
236 		init_pair( 4, COLOR_YELLOW,  COLOR_BLACK);
237 		init_pair( 5, COLOR_BLUE,    COLOR_BLACK);
238 		init_pair( 6, COLOR_MAGENTA, COLOR_BLACK);
239 		init_pair( 7, COLOR_CYAN,    COLOR_BLACK);
240 
241 		init_pair( 8, COLOR_WHITE, COLOR_WHITE  );
242 		init_pair( 9, COLOR_WHITE, COLOR_RED    );
243 		init_pair(10, COLOR_WHITE, COLOR_GREEN, );
244 		init_pair(11, COLOR_WHITE, COLOR_YELLOW );
245 		init_pair(12, COLOR_WHITE, COLOR_BLUE   );
246 		init_pair(13, COLOR_WHITE, COLOR_MAGENTA);
247 		init_pair(14, COLOR_WHITE, COLOR_CYAN   );
248 
249 		this.colors["white"]   = COLOR_PAIR(1);
250 		this.colors["red"]     = COLOR_PAIR(2);
251 		this.colors["green"]   = COLOR_PAIR(3);
252 		this.colors["yellow"]  = COLOR_PAIR(4);
253 		this.colors["blue"]    = COLOR_PAIR(5);
254 		this.colors["magenta"] = COLOR_PAIR(6);
255 		this.colors["cyan"]    = COLOR_PAIR(7);
256 
257 		this.colors["rev-white"]   = COLOR_PAIR(8);
258 		this.colors["rev-red"]     = COLOR_PAIR(9);
259 		this.colors["rev-green"]   = COLOR_PAIR(10);
260 		this.colors["rev-yellow"]  = COLOR_PAIR(11);
261 		this.colors["rev-blue"]    = COLOR_PAIR(12);
262 		this.colors["rev-magenta"] = COLOR_PAIR(13);
263 		this.colors["rev-cyan"]    = COLOR_PAIR(14);
264 	}
265 
266 	void set_status(string status) {
267 		synchronized(this) {
268 			int y, x;
269 			getyx(this.input_window, y, x);
270 
271 			mvwhline(this.input_window, 0, 0, 0, COLS);
272 
273 			int col = 2;
274 
275 			foreach (i, tribune; this.tribunes_ordered) {
276 				if (i == this.active) {
277 					auto style = tribune.updating ? A_REVERSE | A_BOLD : A_REVERSE;
278 					mvwprintw(this.input_window, 0, col, "%s", tribune.tribune.name.toStringz());
279 					mvwchgat(this.input_window, 0, col, cast(int)tribune.tribune.name.length, style, cast(short)tribune.ncolor(false), cast(void*)null);
280 
281 					col += cast(int)tribune.tribune.name.length + 1;
282 				} else {
283 					auto style = tribune.updating ? A_REVERSE : A_BOLD;
284 					mvwprintw(this.input_window, 0, col, "%s", tribune.tribune.name[0 .. 1].toStringz());
285 					mvwchgat(this.input_window, 0, col, 1, style, cast(short)tribune.ncolor(false), cast(void*)null);
286 
287 					col += 2;
288 				}
289 			}
290 
291 			mvwprintw(this.input_window, 0, cast(int)(COLS - 2 - status.length), "%s", status.toStringz());
292 
293 			wmove(this.input_window, y, x);
294 			wrefresh(this.input_window);
295 		}
296 	}
297 
298 	void highlight_post(NCPost post, NCPost origin) {
299 		if (post.offset > this.offset - this.posts_window.maxy) {
300 			int line = this.posts_window.maxy - (this.offset - post.offset);
301 			mvwprintw(this.posts_window, line, 0, ">");
302 			mvwchgat(this.posts_window, line, 0, 1 + 8, A_BOLD, cast(short)post.tribune.ncolor(true), cast(void*)null);
303 			wnoutrefresh(this.posts_window);
304 		}
305 
306 		int scrollresult = scrollok(this.preview_window, true);
307 
308 		int result1 = wclear(this.preview_window);
309 		int result2 = wresize(this.preview_window, post.lines, COLS);
310 		append_post(this.preview_window, post, false, 0);
311 		wresize(this.preview_window, post.lines + 1, COLS);
312 		mvwhline(this.preview_window, post.lines, 0, 0, COLS);
313 
314 		wnoutrefresh(this.preview_window);
315 		top_panel(this.preview_panel);
316 		update_panels();
317 		doupdate();
318 	}
319 
320 	void show_info(NCPost post) {
321 		synchronized(this) {
322 			wmove(this.input_window, 1, 0);
323 			wclrtoeol(this.input_window);
324 		}
325 		string post_info = format("[%s] id=%s ua=%s", post.tribune.tribune.name, post.post.post_id, post.post.info);
326 		if (post_info.count > this.input_window.maxx) {
327 			post_info = post_info[0 .. this.input_window.maxx];
328 		}
329 		mvwprintw(this.input_window, 1, 0, "%s", post_info.toStringz());
330 		wrefresh(this.input_window);
331 	}
332 
333 	void unhighlight_post(NCPost post) {
334 		if (post.offset > this.offset - this.posts_window.maxy) {
335 			int line = this.posts_window.maxy - (this.offset - post.offset);
336 			mvwprintw(this.posts_window, line, 0, " ");
337 			mvwchgat(this.posts_window, line, 0, 1, A_NORMAL, cast(short)post.tribune.ncolor(true), cast(void*)null);
338 			mvwchgat(this.posts_window, line, 1, 8, A_NORMAL, cast(short)1, cast(void*)null);
339 			wnoutrefresh(this.posts_window);
340 		}
341 
342 		top_panel(this.main_panel);
343 		update_panels();
344 		doupdate();
345 	}
346 
347 	void highlight_stop(Stop stop) {
348 		if (stop == Stop.init) {
349 			return;
350 		}
351 
352 		int line = this.posts_window.maxy - (this.offset - stop.offset);
353 		wmove(this.posts_window, line, stop.start);
354 		wchgat(this.posts_window, stop.length, A_REVERSE, 0, null);
355 
356 		wmove(this.input_window, 1, 0);
357 		wnoutrefresh(this.posts_window);
358 		wnoutrefresh(this.input_window);
359 
360 		if (stop.referenced_post) {
361 			this.highlight_post(stop.referenced_post, stop.post);
362 		}
363 
364 		show_info(stop.post);
365 	}
366 
367 	void unhighlight_stop(Stop stop) {
368 		int line = this.posts_window.maxy - (this.offset - stop.offset);
369 		wmove(this.posts_window, line, stop.start);
370 
371 		wchgat(this.posts_window, stop.length, stop.attributes, stop.color, null);
372 
373 		wmove(this.input_window, 1, 0);
374 		wnoutrefresh(this.posts_window);
375 		wnoutrefresh(this.input_window);
376 
377 		if (stop.referenced_post) {
378 			this.unhighlight_post(stop.referenced_post);
379 		} else {
380 			doupdate();
381 		}
382 	}
383 
384 	void loop() {
385 		scope (exit) {
386 			endwin();
387 		}
388 
389 		this.set_status("");
390 
391 		while (true) {
392 			wtimeout(this.input_window, 100);
393 			wmove(this.input_window, 1, 0);
394 
395 			noecho();
396 			keypad(this.input_window, true);
397 			auto ch = wgetch(this.input_window);
398 			switch (ch) {
399 				case KEY_RESIZE:
400 					this.redraw_all_posts();
401 					this.set_status("");
402 					break;
403 				case 0x20:
404 					this.set_status("O");
405 					this.tribunes_ordered[this.active].fetch_posts();
406 					this.set_status("");
407 					break;
408 				case KEY_RIGHT:
409 					this.active++;
410 					if (this.active >= this.tribunes_ordered.length) {
411 						this.active = 0;
412 					}
413 					this.set_status("");
414 					break;
415 				case KEY_LEFT:
416 					this.active--;
417 					// This should be an ulong, but the compiler will optimize
418 					// this out anyway, and it's cleared and safer.
419 					if (this.active < 0 || this.active >= this.tribunes_ordered.length) {
420 						this.active = this.tribunes_ordered.length - 1;
421 					}
422 					this.set_status("");
423 					break;
424 				case KEY_UP:
425 					if (!this.prev_stop()) {
426 						// We're at the top... scroll?
427 					}
428 					break;
429 				case KEY_DOWN:
430 					if (!this.next_stop()) {
431 						// We're at the bottom... unselect everything.
432 						unhighlight_stop(this.current_stop);
433 						this.current_stop = Stop.init;
434 					}
435 					break;
436 				case KEY_HOME:
437 					foreach (Stop stop; this.stops) {
438 						if (stop.offset >= this.offset - this.posts_window.maxy) {
439 							if ((stop.offset < this.current_stop.offset) ||
440 									(stop.offset == this.current_stop.offset && stop.start < this.current_stop.start)) {
441 								set_stop(stop);
442 								break;
443 							}
444 						}
445 					}
446 					break;
447 				case KEY_END:
448 					set_stop(this.stops[$ - 1]);
449 					break;
450 				case 0x0A:
451 					string initial_text = this.current_stop !is Stop.init ? this.current_stop.post.post.clock_ref ~ " " : "";
452 
453 					if (this.current_stop !is Stop.init) {
454 						foreach (n, tribune; this.tribunes_ordered) {
455 							if (tribune == this.current_stop.post.tribune) {
456 								this.active = n;
457 							}
458 						}
459 						this.set_status("");
460 					}
461 
462 					wtimeout(this.input_window, -1);
463 					int exit = 1;
464 					curs_set(2);
465 					string text = uput(this.input_window, 1, 0, COLS - 2, initial_text, "> ", exit);
466 					curs_set(0);
467 					if (exit == 10 && this.tribunes_ordered[this.active].tribune.post(text)) {
468 						this.tribunes_ordered[this.active].fetch_posts();
469 					}
470 					wmove(this.input_window, 1, 0);
471 					wclrtoeol(this.input_window);
472 					wrefresh(this.input_window);
473 					break;
474 				default:
475 					this.display_queued_posts();
476 					break;
477 			}
478 		}
479 	}
480 
481 	void adjust_stop() {
482 		if (this.current_stop.offset < this.offset - this.posts_window.maxy + 1) {
483 			next_stop();
484 		}
485 		highlight_stop(this.current_stop);
486 	}
487 
488 	bool prev_stop() {
489 		Stop new_stop;
490 		Stop old_stop = this.current_stop;
491 		if (this.current_stop is Stop.init && this.stops.length) {
492 			new_stop = this.stops[$ - 1];
493 		} else foreach_reverse (Stop stop; this.stops) {
494 			if (stop.offset >= this.offset - this.posts_window.maxy) {
495 				if ((stop.offset < this.current_stop.offset) ||
496 					(stop.offset == this.current_stop.offset && stop.start < this.current_stop.start)) {
497 					new_stop = stop;
498 					break;
499 				}
500 			}
501 		}
502 
503 		if (new_stop != Stop.init) {
504 			this.set_stop(new_stop);
505 			return true;
506 		}
507 
508 		return false;
509 	}
510 
511 	bool next_stop() {
512 		if (this.current_stop == Stop.init) {
513 			return false;
514 		}
515 
516 		if (this.current_stop == this.stops[$ - 1]) {
517 			return false;
518 		}
519 
520 		Stop new_stop;
521 		Stop old_stop = this.current_stop;
522 		if (this.current_stop is Stop.init) foreach (Stop stop; this.stops) {
523 			if (stop.offset > this.offset - this.posts_window.maxy) {
524 				new_stop = stop;
525 				break;
526 			}
527 		} else foreach (Stop stop; this.stops) {
528 			if (stop.offset > this.offset - this.posts_window.maxy) {
529 				if ((stop.offset > this.current_stop.offset) ||
530 					(stop.offset == this.current_stop.offset && stop.start > this.current_stop.start)) {
531 					new_stop = stop;
532 					break;
533 				}
534 			}
535 		}
536 
537 		if (new_stop != Stop.init) {
538 			this.set_stop(new_stop);
539 		}
540 
541 		return true;
542 	}
543 
544 	void set_stop(Stop stop) {
545 		this.unhighlight_stop(this.current_stop);
546 		this.current_stop = stop;
547 		this.highlight_stop(this.current_stop);
548 	}
549 
550 	int append_post(WINDOW* window, NCPost post, bool add_stops, int offset) {
551 		int offset_start = offset;
552 		offset++;
553 
554 		wscrl(window, 1);
555 
556 		int x = 0;
557 		string clock = post.post.clock;
558 
559 		mvwprintw(window, window.maxy, x, " ");
560 		mvwchgat(window, cast(int)window.maxy, x, 1, A_NORMAL, cast(short)post.tribune.ncolor(true), cast(void*)null);
561 		x += 1;
562 		mvwprintw(window, window.maxy, x, "%s", clock.toStringz());
563 		int clock_len = cast(int)std.utf.count(clock);
564 		ulong current_attributes;
565 		short pair;
566 		int opts;
567 		wattr_get(window, &current_attributes, &pair, cast(void*)&opts);
568 
569 		Stop post_stop = Stop(offset, x, clock_len, post, current_attributes, pair);
570 		x += clock_len;
571 
572 		mvwprintw(window, window.maxy, x, " ");
573 		x += 1;
574 
575 		if (post.post.login.length > 0) {
576 			int count = cast(int) post.post.login.count;
577 			mvwprintw(window, window.maxy, x, "%s", post.post.login.toStringz());
578 			mvwchgat(window, cast(int)window.maxy, x, count, A_BOLD, cast(short)0, cast(void*)null);
579 			x += count;
580 		} else {
581 			int count = cast(int) post.post.short_info.count;
582 			mvwprintw(window, window.maxy, x, "%s", post.post.short_info.toStringz());
583 			mvwchgat(window, cast(int)window.maxy, x, count, this.colors["rev-white"] | A_REVERSE | A_BOLD, cast(short)0, cast(void*)null);
584 			x += count;
585 		}
586 
587 		mvwprintw(window, window.maxy, x, "> ");
588 		x += 2;
589 
590 		string[] tokens = post.tokenize();
591 
592 		bool has_clocks = false;
593 		foreach (int i, string sub; tokens) {
594 			auto length = sub.count;
595 
596 			// No need to scroll ourselves if the word is
597 			// longer than screen, we'll let ncurses take
598 			// care of wrapping it where it likes.
599 			// But if it's smaller and it's going to end
600 			// outside the screen, then scroll and print
601 			// it with some indentation.
602 			if (x + length >= COLS && length <= (COLS - 2)) {
603 				x = 2;
604 				wscrl(window, 1);
605 				offset++;
606 
607 				// Add leading color marker
608 				mvwprintw(window, window.maxy, 0, " ");
609 				mvwchgat(window, cast(int)window.maxy, 0, 1, A_NORMAL, cast(short)post.tribune.ncolor(true), cast(void*)null);
610 
611 				wmove(window, window.maxy, 1);
612 			}
613 
614 			bool is_clock = false;
615 
616 			foreach (Clock post_clock; post.post.clocks) {
617 				if (sub.strip == post_clock.text) {
618 					NCTribune ref_tribune = post.tribune;
619 
620 					wattr_get(window, &current_attributes, &pair, cast(void*)&opts);
621 					if (!(current_attributes & A_BOLD)) {
622 						wattron(window, A_BOLD);
623 						is_clock = true;
624 					}
625 
626 					has_clocks = true;
627 
628 					wattr_get(window, &current_attributes, &pair, cast(void*)&opts);
629 
630 					if (add_stops) {
631 						if (post_clock.tribune.length > 1) {
632 							if (post_clock.tribune in this.tribunes) {
633 								ref_tribune = this.tribunes[post_clock.tribune];
634 							} else foreach (NCTribune t; this.tribunes) {
635 								if (find(t.tribune.aliases, post_clock.tribune).length > 0) {
636 									ref_tribune = t;
637 									break;
638 								}
639 							}
640 						}
641 
642 						this.stops ~= Stop(offset, x, cast(int)sub.count, post, current_attributes, pair, ref_tribune.find_referenced_post(post_clock.time, post_clock.index));
643 					}
644 				}
645 			}
646 
647 			switch (sub.strip()) {
648 				case "<b>":
649 					wattron(window, A_BOLD);
650 					break;
651 				case "</b>":
652 					wattroff(window, A_BOLD);
653 					break;
654 				case "<i>":
655 					wattron(window, this.colors["cyan"]);
656 					break;
657 				case "</i>":
658 					wattroff(window, this.colors["cyan"]);
659 					break;
660 				case "<u>":
661 					wattron(window, A_UNDERLINE);
662 					break;
663 				case "</u>":
664 					wattroff(window, A_UNDERLINE);
665 					break;
666 				default:
667 					mvwprintw(window, window.maxy, x, "%s", sub.toStringz());
668 					x += length;
669 					break;
670 			}
671 
672 			if (is_clock) {
673 				wattroff(window, A_BOLD);
674 			}
675 
676 			if (length >= (COLS - 2)) {
677 				// Then the text will have wrapped several times.
678 				offset += x/COLS;
679 				x = x%COLS;
680 			}
681 		}
682 
683 		if (!has_clocks && add_stops) {
684 			this.stops ~= post_stop;
685 		}
686 
687 		wattrset(window, A_NORMAL);
688 
689 		wrefresh(window);
690 
691 		post.lines = offset - offset_start;
692 
693 		foreach (int i, Stop stop; this.stops) {
694 			if (stop.offset < offset_start - (LINES * 10) && i+1 < this.stops.length) {
695 				this.stops = this.stops[0 .. i] ~ this.stops[i+1 .. $];
696 			}
697 		}
698 
699 		return offset;
700 	}
701 
702 	NCPost[] posts_to_display;
703 	void enqueue_post(NCPost post) {
704 		this.posts_to_display ~= post;
705 	}
706 
707 	void display_queued_posts() {
708 		foreach (NCPost post; this.posts_to_display) {
709 			this.display_post(this.posts_window, post);
710 		}
711 
712 		this.posts_to_display.length = 0;
713 	}
714 
715 	void display_post(WINDOW* window, NCPost post, bool add_stops = true, bool scroll = true) {
716 		if (this.display_enabled && !this.is_post_ignored(post)) {
717 			synchronized(this) {
718 				post.offset = this.offset + 1;
719 
720 				this.offset = append_post(window, post, add_stops, this.offset);
721 				if (scroll) {
722 					adjust_stop();
723 				}
724 			}
725 		}
726 	}
727 
728 	NCPost[] ignored_posts;
729 
730 	bool is_post_ignored(NCPost post) {
731 		if (this.config.default_ignorelist.find(post.post.login).length > 0
732 		 || this.config.default_ignorelist.find(post.post.info).length > 0) {
733 			this.ignored_posts ~= post;
734 
735 			return true;
736 		}
737 
738 		foreach (Clock clock; post.post.clocks) {
739 			NCTribune ref_tribune = post.tribune;
740 			if (clock.tribune in this.tribunes) {
741 				ref_tribune = this.tribunes[clock.tribune];
742 			} else foreach (NCTribune t; this.tribunes) {
743 				if (find(t.tribune.aliases, clock.tribune).length > 0) {
744 					ref_tribune = t;
745 					break;
746 				}
747 			}
748 
749 			if (ref_tribune.find_referenced_post(clock.time, clock.index, this.ignored_posts)) {
750 				this.ignored_posts ~= post;
751 
752 				return true;
753 			}
754 		}
755 
756 		return false;
757 	}
758 }
759 
760 class NCTribune {
761 	Tribune tribune;
762 	NCUI ui;
763 	int _color;
764 	NCPost[] posts;
765 
766 	bool updating;
767 
768 	this(NCUI ui, Tribune tribune) {
769 		this.ui = ui;
770 		this.tribune = tribune;
771 		this.tribune.new_post.connect(&this.on_new_post);
772 	}
773 
774 	void color(int c) {
775 		this._color = c;
776 	}
777 
778 	ulong color(bool invert = false) {
779 		return COLOR_PAIR(invert ? this._color + 7 : this._color);
780 	}
781 
782 	int ncolor(bool invert = false) {
783 		return invert ? this._color + 7 : this._color;
784 	}
785 
786 	void on_new_post(Post post) {
787 		NCPost p = new NCPost(this, post);
788 		this.posts ~= p;
789 
790 		if (this.posts.length > LINES * 10) {
791 			this.posts = this.posts[1 .. $];
792 		}
793 
794 		this.ui.enqueue_post(p);
795 	};
796 
797 	auto fetch_posts() {
798 		this.updating = true;
799 
800 		this.tribune.fetch_posts();
801 
802 		this.updating = false;
803 	}
804 
805 	NCPost find_referenced_post(string clock, int index = 1, NCPost[] posts = null) {
806 		if (posts is null) {
807 			posts = this.posts;
808 		}
809 
810 		NCPost[] matching;
811 		foreach_reverse(NCPost post; posts) {
812 			if (clock.length > 5 && post.post.clock == clock) {
813 				matching ~= post;
814 			} else if (clock.length == 5 && post.post.clock[0 .. 5] == clock) {
815 				matching ~= post;
816 			} else if (matching.length > 0) {
817 				// We have already found at least one matching post,
818 				// and this one doesn't match, so any further matching
819 				// post would be an older, not consecutive, post.
820 				break;
821 			}
822 		}
823 
824 		index = cast(int)(matching.length - index);
825 
826 		if (index >= 0) {
827 			return matching[index];
828 		}
829 
830 		return null;
831 	}
832 }
833 
834 class NCPost {
835 	NCTribune tribune;
836 	Post post;
837 
838 	int offset;
839 	int lines;
840 
841 	this(NCTribune tribune, Post post) {
842 		this.tribune = tribune;
843 		this.post = post;
844 	}
845 
846 	string[] tokenize() {
847 		string line = this.post.message.replace(regex(`\s+`, "g"), " ");
848 
849 		// Since I can't use backreferences here...
850 		line = line.replace(regex(`<a href="(.*?)".*?>(.*?)</a>`, "g"), "<$1>");
851 		line = line.replace(regex(`<a href='(.*?)'.*?>(.*?)</a>`, "g"), "<$1>");
852 
853 		line = std.array.replace(line, "&lt;", "<");
854 		line = std.array.replace(line, "&gt;", ">");
855 		line = std.array.replace(line, "&amp;", "&");
856 
857 		string[] tokens = [""];
858 
859 		bool next = false;
860 		foreach (char c; line) {
861 			switch (c) {
862 				case '<':
863 					tokens ~= "";
864 					break;
865 				case '{':
866 				case '[':
867 				case '(':
868 				case ' ':
869 				case ',':
870 					tokens ~= "";
871 					next = true;
872 					break;
873 				case ']':
874 				case '>':
875 					next = true;
876 					break;
877 				default:
878 					break;
879 			}
880 
881 			tokens[$-1] ~= c;
882 
883 			if (next) {
884 				tokens ~= "";
885 				next = false;
886 			}
887 		}
888 
889 		return tokens;
890 	}
891 }