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, ¤t_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, ¤t_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, ¤t_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, "<", "<"); 854 line = std.array.replace(line, ">", ">"); 855 line = std.array.replace(line, "&", "&"); 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 }