1 /* Main module. 2 3 Provides Curses, Window and ColorTable classes. 4 5 */ 6 7 module nice.curses; 8 9 import std.uni; 10 11 import deimos.ncurses; 12 public import deimos.ncurses: chtype, wint_t; 13 14 package alias nc = deimos.ncurses; /* Just for convenience. */ 15 16 /* ---------- base stuff ---------- */ 17 18 /* The class representing current library state. Make sure you only have a 19 single instance of this around. */ 20 final class Curses 21 { 22 private: 23 Window[] windows; 24 Config cfg; 25 Mode curMode; 26 27 public: 28 Window stdscr; 29 ColorTable colors; 30 static bool echoMode = true; 31 32 /* This struct controls initialization and finalization of the library. */ 33 static struct Config 34 { 35 bool useColors = true; 36 bool useStdColors = true; 37 bool disableEcho = false; 38 Mode mode = Mode.normal; 39 int cursLevel = 1; 40 bool initKeypad = true; 41 bool nl = false; 42 bool nodelay = false; 43 } 44 45 static enum Mode 46 { 47 normal, 48 cbreak, 49 halfdelay, 50 raw, 51 } 52 53 /* Initialize the library. */ 54 this(Config config = Config()) 55 { 56 import core.stdc.locale; 57 cfg = config; 58 setlocale(LC_ALL, ""); 59 60 stdscr = new Window(null, initscr()); 61 if (config.useColors) { 62 import std.exception; 63 enforce(has_colors(), "Terminal does not support colors"); 64 start_color(); 65 colors = new ColorTable(config.useStdColors); 66 stdscr.colors = colors; 67 } 68 69 if (config.disableEcho) echo(false); 70 setMode(config.mode); 71 curs_set(config.cursLevel); 72 if (config.initKeypad) stdscr.keypad(true); 73 if (config.nl) 74 nc.nl(); 75 else 76 nc.nonl(); 77 } 78 79 /* Finalize the library. Consider adding 80 'scope(exit) destroy(cursesState)` 81 to the top of yours program to automatically clean-up the library, 82 even in case of exceptions. 83 */ 84 ~this() 85 { 86 if (cfg.initKeypad) stdscr.keypad(false); 87 final switch (curMode) { 88 case Mode.normal: break; 89 case Mode.cbreak: nocbreak(); break; 90 case Mode.halfdelay: nocbreak(); break; 91 case Mode.raw: noraw(); break; 92 } 93 curs_set(1); 94 if (cfg.disableEcho) echo(true); 95 nc.nl(); 96 endwin(); 97 } 98 99 /* ---------- window manipulation ---------- */ 100 101 Window newWindow(int nlines, int ncols, int begin_y, int begin_x) 102 { 103 Window res = new Window(colors, nlines, ncols, begin_y, begin_x, cfg.initKeypad); 104 windows ~= res; 105 return res; 106 } 107 108 Window duplicateWindow(Window which) 109 { 110 auto p = dupwin(which.ptr); 111 Window res = new Window(colors, p, cfg.initKeypad); 112 windows ~= res; 113 return res; 114 } 115 116 void deleteWindow(Window which) 117 { 118 import std.algorithm; 119 import std.array; 120 windows = array(windows.remove!(w => w is which)); 121 } 122 123 /* ---------- general commands ---------- */ 124 125 static void beep() 126 { 127 nc.beep(); 128 } 129 130 static void delayOutput(int ms) 131 { 132 nc.delay_output(ms); 133 } 134 135 static void echo(bool set) 136 { 137 if (set) 138 nc.echo(); 139 else 140 nc.noecho(); 141 Curses.echoMode = set; 142 } 143 144 static void flash() 145 { 146 nc.flash(); 147 } 148 149 static void flushInput() 150 { 151 nc.flushinp(); 152 } 153 154 static void nap(int ms) 155 { 156 nc.napms(ms); 157 } 158 159 static void resetTTY() 160 { 161 nc.resetty(); 162 } 163 164 static void saveTTY() 165 { 166 nc.savetty(); 167 } 168 169 static void setCursor(int level) 170 { 171 nc.curs_set(level); 172 } 173 174 void setMode(Mode mode, int delayForHD = 1) 175 { 176 final switch (mode) { 177 case Mode.normal: break; 178 case Mode.cbreak: nc.cbreak(); break; 179 case Mode.halfdelay: nc.halfdelay(delayForHD); break; 180 case Mode.raw: nc.raw(); break; 181 } 182 curMode = mode; 183 } 184 185 static void ungetch(int ch) 186 { 187 nc.ungetch(ch); 188 } 189 190 static void update() 191 { 192 nc.doupdate(); 193 } 194 195 /* ---------- some constants ---------- */ 196 197 static int lines() 198 { 199 return nc.LINES; 200 } 201 202 static int cols() 203 { 204 return nc.COLS; 205 } 206 207 static int maxColors() 208 { 209 return nc.COLORS; 210 } 211 212 static int maxColorPairs() 213 { 214 return nc.COLOR_PAIRS; 215 } 216 217 static int tabSize() 218 { 219 return nc.TABSIZE; 220 } 221 222 static int escDelay() 223 { 224 return nc.ESCDELAY; 225 } 226 227 /* ---------- information retrieval ---------- */ 228 229 static int baudrate() 230 { 231 return nc.baudrate(); 232 } 233 234 static bool canChangeColor() 235 { 236 return nc.can_change_color(); 237 } 238 239 static RGB colorContent(short color) 240 { 241 short r, g, b; 242 color_content(color, &r, &g, &b); 243 return RGB(r, g, b); 244 } 245 246 static bool hasColors() 247 { 248 return nc.has_colors(); 249 } 250 251 static string keyName(int key) 252 { 253 import std.string; 254 return nc.keyname(key).fromStringz.idup; 255 } 256 } 257 258 /* The class representing a window. */ 259 final class Window 260 { 261 private: 262 WINDOW* ptr; 263 Window[] children; 264 Window parent; 265 bool isKeypad; 266 267 package: 268 this(ColorTable colors, WINDOW* fromPtr, bool setKeypad = true) 269 { 270 ptr = fromPtr; 271 this.colors = colors; 272 keypad(setKeypad); 273 isKeypad = setKeypad; 274 } 275 276 this(ColorTable colors, int nlines, int ncols, int y, int x, bool setKeypad = true) 277 { 278 ptr = newwin(nlines, ncols, y, x); 279 this.colors = colors; 280 keypad(setKeypad); 281 isKeypad = setKeypad; 282 } 283 284 ~this() 285 { 286 keypad(false); 287 delwin(ptr); 288 } 289 290 public: 291 ColorTable colors; 292 293 /* ---------- general manipulation ---------- */ 294 295 void keypad(bool set) 296 { 297 if (nc.keypad(ptr, set) != OK) { 298 if (set) 299 throw new NCException("Failed to enable keypad mode"); 300 else 301 throw new NCException("Failed to disable keypad mode"); 302 } 303 } 304 305 /* This one moves the cursor. */ 306 void move(int y, int x) 307 { 308 if (wmove(ptr, y, x) != OK) 309 throw new NCException("Failed to move to position %s:%s", y, x); 310 } 311 312 /* This one moves the window. */ 313 void moveWindow(int y, int x) 314 { 315 if (mvwin(ptr, y, x) != OK) 316 throw new NCException("Failed to move a window to position %s:%s", y, x); 317 } 318 319 void refresh() 320 { 321 nc.wnoutrefresh(ptr); 322 } 323 324 /* ---------- child windows management ---------- */ 325 326 Window subwin(int nlines, int ncols, int y, int x) 327 { 328 WINDOW* p = nc.subwin(ptr, nlines, ncols, y, x); 329 if (p is null) 330 throw new NCException( 331 "Failed to create a subwindow at %s:%s with height %s and width %s", 332 y, x, nlines, ncols); 333 Window res = new Window(colors, p, isKeypad); 334 res.parent = this; 335 children ~= res; 336 return res; 337 } 338 339 Window derwin(int nlines, int ncols, int y, int x) 340 { 341 WINDOW* p = nc.derwin(ptr, nlines, ncols, y, x); 342 if (p is null) 343 throw new NCException( 344 "Failed to create a subwindow at %s:%s with height %s and width %s", 345 y, x, nlines, ncols); 346 Window res = new Window(colors, p, isKeypad); 347 res.parent = this; 348 children ~= res; 349 return res; 350 } 351 352 void deleteChild(Window child) 353 { 354 import std.algorithm; 355 import std.array; 356 children = array(children.remove!(c => c is child)); 357 } 358 359 /* ---------- drawing ---------- */ 360 361 import std.range; 362 import std.traits; 363 364 enum isString(T) = isInputRange!T && is(Unqual!(ElementType!T): dchar); 365 enum isAttrRange(T) = isInputRange!T && is(Unqual!(ElementType!T): chtype); 366 367 void addch(C: cchar_t)(C ch) 368 { 369 bool isLowerRight = (curY == height - 1) && (curX == width - 1); 370 cchar_t cchar = ch; 371 if (nc.wadd_wch(ptr, &cchar) != OK && !isLowerRight) 372 throw new NCException("Failed to add complex character '%s'", ch); 373 374 } 375 376 void addch(C: wint_t, A: chtype)(C ch, A attr = Attr.normal) 377 { 378 bool isLowerRight = (curY == height - 1) && (curX == width - 1); 379 auto toDraw = prepChar(ch, attr); 380 if (nc.wadd_wch(ptr, &toDraw) != OK && !isLowerRight) 381 throw new NCException("Failed to add character '%s'", ch); 382 } 383 384 void addch(C: cchar_t)(int y, int x, C ch) 385 { 386 try { 387 move(y, x); 388 addch(ch); 389 } catch (NCException e) { 390 throw new NCException("Failed to add complex character '%s' at %s%s", 391 ch, y, x); 392 } 393 } 394 395 void addch(C: wint_t, A: chtype)(int y, int x, C ch, A attr = Attr.normal) 396 { 397 try { 398 move(y, x); 399 addch(ch, attr); 400 } catch (NCException e) { 401 throw new NCException("Failed to add character '%s' at %s:%s", ch, y, x); 402 } 403 } 404 405 /* Coords, n, multiple attrs */ 406 void addnstr(String, Range)(int y, int x, String str, int n, Range attrs, 407 OOB onOOB = OOB.ignore) 408 if (isString!String && isAttrRange!Range) 409 { 410 /* Move first. */ 411 try { 412 move(y, x); 413 } catch (NCException e) { 414 throw new NCException("Failed to write string '%s' at %s:%s", str, y, x); 415 } 416 /* Write second. */ 417 try { 418 addnstr(str, n, attrs); 419 } catch (NCException e) { 420 if (onOOB == OOB.except) 421 throw new NCException("An out-of-bounds condition was " ~ 422 "encountered when writing string '%s' at %s:%s", 423 str, y, x); 424 } 425 } 426 427 /* n, multiple attrs */ 428 void addnstr(String, Range)(String str, int n, Range attrs, 429 OOB onOOB = OOB.ignore) 430 if (isString!String && isAttrRange!Range) 431 { 432 import std.array; 433 import std.conv; 434 import std.range; 435 import std.uni; 436 auto grs = str.byGrapheme; 437 foreach (gr; str.byGrapheme) { 438 if (n <= 0) break; 439 if (attrs.empty) break; 440 string chr = text(gr[].array); 441 auto attr = attrs.front; 442 attrs.popFront; 443 auto c = CChar(chr, attr); 444 try { 445 addch(c); 446 } catch (NCException e) { 447 if (onOOB == OOB.ignore) 448 break; 449 else 450 throw new NCException("An out-of-bounds condition was " ~ 451 "encountered when writing string '%s'", str); 452 } 453 n--; 454 } /* foreach grapheme */ 455 } 456 457 /* Coords, n, single attr */ 458 void addnstr(String, A: chtype) 459 (int y, int x, String str, int n, A attr = Attr.normal, OOB onOOB = OOB.ignore) 460 if (isString!String) 461 { 462 try { 463 move(y, x); 464 } catch (NCException e) { 465 throw new NCException("Failed to write string '%s' at %s:%s", str, y, x); 466 } 467 try { 468 addnstr(str, n, attr); 469 } catch (NCException e) { 470 if (onOOB == OOB.except) 471 throw new NCException("An out-of-bounds condition was " ~ 472 "encountered when writing string '%s' at %s:%s", 473 str, y, x); 474 } /* try write */ 475 } 476 477 /* n, single attr */ 478 void addnstr(String, A: chtype)(String str, int n, A attr = Attr.normal, 479 OOB onOOB = OOB.ignore) 480 if (isString!String) 481 { 482 import core.stdc.stddef; 483 import std.array; 484 import std.conv; 485 import std.range; 486 import std.uni; 487 488 scope(exit) setAttr(Attr.normal); 489 setAttr(attr); 490 wchar_t[] chars = []; 491 chars.reserve(str.walkLength); 492 foreach (gr; str.byGrapheme) 493 chars ~= gr[].array.to!(wchar_t[]); 494 chars ~= 0; 495 if (waddwstr(ptr, chars.ptr) != OK && onOOB == OOB.except) 496 throw new NCException("Failed to add string '%s'", str); 497 } 498 499 /* Coords, multiple attrs */ 500 void addstr(String, Range)(int y, int x, String str, Range attrs, 501 OOB onOOB = OOB.ignore) 502 if (isString!String && isAttrRange!Range) 503 { 504 addnstr(y, x, str, width * height, attrs, onOOB); 505 } 506 507 /* Multiple attrs */ 508 void addstr(String, Range)(String str, Range attrs, OOB onOOB = OOB.ignore) 509 if (isString!String && isAttrRange!Range) 510 { 511 addnstr(str, width * height, attrs, onOOB); 512 } 513 514 /* Coords, single attr */ 515 void addstr(String, A: chtype)(int y, int x, String str, A attr = Attr.normal, 516 OOB onOOB = OOB.ignore) 517 if (isString!String) 518 { 519 addnstr(y, x, str, width * height, attr, onOOB); 520 } 521 522 /* Single attr */ 523 void addstr(String, A: chtype)(String str, A attr = Attr.normal, 524 OOB onOOB = OOB.ignore) 525 if (isString!String) 526 { 527 addnstr(str, width * height, attr, onOOB); 528 } 529 530 /* 531 The exact behaviour depends on the 'alignment' parameter. 532 If it's Align.left, then y and x are the coordinates of text's 533 upper left corner. 534 If it's Align.center, then they are the coordinates of text's first 535 line's center. 536 If it's Align.right, then they are the coordinates of text's upper 537 right corner. 538 539 Note that this method uses the entire window. 540 */ 541 void addAligned(String, Range)(int y, int x, String str, 542 Align alignment, Range attrs, OOB onOOB = OOB.ignore) 543 if (isString!String && isAttrRange!Range) 544 { 545 import std.algorithm; 546 import std.range; 547 import std.string; 548 import std.uni; 549 550 /* Advance to the next line. */ 551 void nextLine(Range)(Range grs) 552 { 553 final switch (alignment) { 554 case Align.left: 555 move(y, x); 556 break; 557 case Align.center: 558 int offset = min(grs.walkLength / 2 - 1, x); 559 move(y, x - offset); 560 break; 561 case Align.right: 562 int offset = min(x, grs.walkLength - 1); 563 move(y, x - offset); 564 break; 565 } 566 } 567 /* The 'take' is here for infinite ranges. Not sure why would 568 anyone want to feed an infinite range to this method, but hey. 569 It's nice to have the possibility. 570 */ 571 auto arr = str.take(width * height).array; 572 foreach (line; arr.splitLines) { 573 auto grs = line.byGrapheme; 574 nextLine(grs); 575 while (!grs.empty && !attrs.empty) { 576 auto gr = grs.front; 577 auto attr = attrs.front; 578 grs.popFront; 579 attrs.popFront; 580 try { 581 addch(fromGrapheme(gr, attr)); 582 } catch (NCException e) { 583 if (onOOB == OOB.except) 584 throw new NCException("An out-of-bounds condition " ~ 585 "was encountered when adding aligned string '%s'" ~ 586 "at %s:%s", str, y, x); 587 else 588 return; 589 } 590 if (curY >= height) return; 591 if (curY > y) { 592 y = curY; 593 nextLine(grs); 594 } /* if next line */ 595 } /* while !grs.empty */ 596 y++; 597 } /* foreach line */ 598 } /* addAligned */ 599 600 void addAligned(String, A: chtype)(int y, int x, String str, 601 Align alignment, A attr = Attr.normal, OOB onOOB = OOB.ignore) 602 if (isString!String) 603 { 604 import std.range; 605 addAligned(y, x, str, alignment, cycle([attr]), onOOB); 606 } 607 608 /* Use the whole window and figure out exact X coordinate. */ 609 void addAligned(String, Range) (int y, String str, Align alignment, 610 Range attrs, OOB onOOB = OOB.ignore) 611 if (isString!String && isAttrRange!Range) 612 { 613 final switch(alignment) { 614 case Align.left: 615 addAligned(y, 0, str, alignment, attrs, onOOB); 616 break; 617 case Align.center: 618 addAligned(y, width / 2, str, alignment, attrs, onOOB); 619 break; 620 case Align.right: 621 addAligned(y, width - 1, str, alignment, attrs, onOOB); 622 break; 623 } 624 } 625 626 /* Ditto, but with single attribute. */ 627 void addAligned(String, A: chtype) (int y, String str, Align alignment, 628 A attr = Attr.normal, OOB onOOB = OOB.ignore) 629 if (isString!String) 630 { 631 import std.range; 632 633 addAligned(y, str, alignment, cycle([attr]), onOOB); 634 } 635 636 void border(chtype left, chtype right, chtype top, chtype bottom, 637 chtype topLeft, chtype topRight, 638 chtype bottomLeft, chtype bottomRight) 639 { 640 nc.wborder(ptr, left, right, top, bottom, topLeft, topRight, bottomLeft, bottomRight); 641 } 642 643 void border(cchar_t left, cchar_t right, cchar_t top, cchar_t bottom, 644 cchar_t topLeft, cchar_t topRight, 645 cchar_t bottomLeft, cchar_t bottomRight) 646 { 647 nc.wborder_set(ptr, 648 &left, &right, &top, &bottom, 649 &topLeft, &topRight, &bottomLeft, &bottomRight); 650 } 651 652 void box(cchar_t vertical, cchar_t horizontal) 653 { 654 nc.box_set(ptr, &vertical, &horizontal); 655 } 656 657 void box(chtype v, chtype h) 658 { 659 nc.box(ptr, v, h); 660 } 661 662 void delch(int y, int x) 663 { 664 if (nc.mvwdelch(ptr, y, x) != OK) 665 throw new NCException("Failed to delete character from position %s:%s", y, x); 666 } 667 668 void delch() 669 { 670 nc.wdelch(ptr); 671 } 672 673 void insert(chtype ch) 674 { 675 nc.winsch(ptr, ch); 676 } 677 678 void insert(int y, int x, chtype ch) 679 { 680 if (nc.mvwinsch(ptr, y, x, ch) != OK) 681 throw new NCException("Failed to insert a character at position %s:%s", y, x); 682 } 683 684 void insert(string s) 685 { 686 import std.string; 687 /* This is unfortunate, but none of 'insstr' family functions 688 accept immutable strings, the result of toStringz. 689 */ 690 nc.winsstr(ptr, cast(char*) (s.toStringz)); 691 } 692 693 void insert(int y, int x, string s) 694 { 695 import std.string; 696 if (nc.mvwinsstr(ptr, y, x, cast(char*) (s.toStringz)) != OK) 697 throw new NCException("Failed to insert a string at position %s:%s", y, x); 698 } 699 700 void insert(string s, int n) 701 { 702 import std.string; 703 nc.winsnstr(ptr, cast(char*) (s.toStringz), n); 704 } 705 706 void insert(int y, int x, string s, int n) 707 { 708 import std.string; 709 /* This fails to compile if template parameters list is omitted. 710 Dunno why. 711 */ 712 if (nc.mvwinsnstr!(WINDOW, int, char)(ptr, y, x, cast(char*) (s.toStringz), n) 713 != OK) 714 throw new NCException("Failed to insert a string at position %s:%s", y, x); 715 } 716 717 void hline(int y, int x, chtype ch, int n) 718 { 719 if (nc.mvwhline(ptr, y, x, ch, n) != OK) 720 throw new NCException("Failed to draw a horizontal line at %s:%s", y, x); 721 } 722 723 void hline(int y, int x, cchar_t ch, int n) 724 { 725 try { 726 move(y, x); 727 hline(ch, n); 728 } catch (NCException e) { 729 throw new NCException("Failed to draw a horizontal line at %s:%s", y, x); 730 } 731 } 732 733 void hline(chtype ch, int n) 734 { 735 nc.whline(ptr, ch, n); 736 } 737 738 void hline(cchar_t ch, int n) 739 { 740 nc.whline_set(ptr, &ch, n); 741 } 742 743 /* Overlays this window on top of another (non-destructively). */ 744 void overlay(Window target) 745 { 746 if (nc.overlay(ptr, target.ptr) == ERR) 747 throw new NCException("Failed to overlay a window"); 748 } 749 750 /* Overwrites this window on top of another (destructively). */ 751 void overwrite(Window target) 752 { 753 if (nc.overwrite(ptr, target.ptr) == ERR) 754 throw new NCException("Failed to overwrite a window"); 755 } 756 757 private void setAttr(A: chtype)(A attr = Attr.normal) 758 { 759 if (wattrset(ptr, attr) != OK) 760 throw new NCException("Failed to set attribute '%s'", attr); 761 } 762 763 void vline(int y, int x, chtype ch, int n) 764 { 765 if (nc.mvwvline(ptr, y, x, ch, n) != OK) 766 throw new NCException("Failed to draw a vertical line at %s:%s", y, x); 767 } 768 769 void vline(int y, int x, cchar_t ch, int n) 770 { 771 try { 772 move(y, x); 773 vline(ch, n); 774 } catch (NCException e) { 775 throw new NCException("Failed to draw a vertical line at %s:%s", y, x); 776 } 777 } 778 779 void vline(chtype ch, int n) 780 { 781 nc.wvline(ptr, ch, n); 782 } 783 784 void vline(cchar_t ch, int n) 785 { 786 nc.wvline_set(ptr, &ch, n); 787 } 788 789 /* ---------- various manipulations ---------- */ 790 791 void bkgd(chtype ch) 792 { 793 nc.wbkgd(ptr, ch); 794 } 795 796 void bkgdset(chtype ch) 797 { 798 nc.wbkgdset(ptr, ch); 799 } 800 801 void clear() 802 { 803 nc.wclear(ptr); 804 } 805 806 void clearToBottom() 807 { 808 nc.wclrtobot(ptr); 809 } 810 811 void clearToEOL() 812 { 813 nc.wclrtoeol(ptr); 814 } 815 816 void deleteln() 817 { 818 nc.wdeleteln(ptr); 819 } 820 821 void erase() 822 { 823 nc.werase(ptr); 824 } 825 826 void insdel(int n) 827 { 828 nc.winsdelln(ptr, n); 829 } 830 831 void insertln() 832 { 833 nc.winsertln(ptr); 834 } 835 836 void scroll(int n) 837 { 838 if (nc.wscrl(ptr, n) != OK) 839 throw new NCException("Failed to scroll a window by %s lines", n); 840 } 841 842 void timeout(int ms) 843 { 844 nc.wtimeout(ptr, ms); 845 } 846 847 /* ---------- information retrieval ---------- */ 848 849 int getch() 850 { 851 int res = wgetch(ptr); 852 if (res == ERR) 853 throw new NCException("Failed to get a character"); 854 return res; 855 } 856 857 /* This should be preferred over plain 'getch' if you care about your 858 program being Unicode-aware. 859 */ 860 WChar getwch() 861 { 862 wint_t chr; 863 int res = wget_wch(ptr, &chr); 864 if (res == KEY_CODE_YES) 865 return WChar(chr, true); 866 else if (res == OK) 867 return WChar(chr, false); 868 else 869 throw new NCException("Failed to get a wide character"); 870 } 871 872 int begX() @property 873 { 874 return nc.getbegx(ptr); 875 } 876 877 int begY() @property 878 { 879 return nc.getbegy(ptr); 880 } 881 882 int curX() @property 883 { 884 return nc.getcurx(ptr); 885 } 886 887 int curY() @property 888 { 889 return nc.getcury(ptr); 890 } 891 892 int width() @property 893 { 894 return nc.getmaxx(ptr) + 1; 895 } 896 897 int height() @property 898 { 899 return nc.getmaxy(ptr) + 1; 900 } 901 902 string getstr(int maxLength, bool echoChars = true) 903 { 904 import std.conv; 905 906 bool isEcho = Curses.echoMode; 907 Curses.echo(echoChars); 908 scope(exit) Curses.echo(isEcho); 909 910 wint_t[] buffer = new wint_t[maxLength + 1]; 911 buffer[$ - 1] = 0; 912 if (wgetn_wstr(ptr, buffer.ptr, maxLength) != OK) 913 throw new NCException("Failed to get a string"); 914 string res; 915 res.reserve(buffer.length); 916 foreach (ch; buffer) 917 res ~= text(ch); 918 return res; 919 } 920 921 string getstr(bool echoChars = true) 922 { 923 return getstr(ch => true, echoChars); 924 } 925 926 string getstr(bool delegate(wint_t) predicate, bool echoChars = true) 927 { 928 import std.conv; 929 930 bool isEcho = Curses.echoMode; 931 Curses.echo(false); 932 scope(exit) Curses.echo(isEcho); 933 934 string res; 935 int x = curX; 936 int y = curY; 937 while(true) { 938 WChar key; 939 try { 940 key = getwch; 941 } catch (NCException e) { 942 return res; 943 } 944 bool special = key.isSpecialKey; 945 wint_t ch = key.chr; 946 /* Check for end-of-input keys. */ 947 if ((special && key.key == Key.enter) 948 || ch == '\n' || ch == '\r') { 949 return res; 950 } 951 /* Check for erase-some-characters keys. */ 952 wint_t kill, erase; 953 nc.killwchar(&kill); 954 nc.erasewchar(&erase); 955 if (special && (key.key == Key.left || key.key == Key.backspace) 956 || ch == '\b' || ch == kill || ch == erase) { 957 if (res.length == 0) continue; 958 import std.array; 959 import std.range; 960 import std.uni; 961 res = res 962 .byGrapheme 963 .array 964 .dropBackOne 965 .byCodePoint 966 .text; 967 /* Delete the character under the cursor and then move it. */ 968 if (!echoChars) continue; 969 x--; 970 if (x < 0) { 971 x = Curses.cols - 1; 972 y--; 973 } 974 if (y < 0) y = 0; 975 delch(y, x); 976 continue; 977 } 978 if (special) continue; 979 /* When at the end of the window, allow only deleting and 980 finishing the input. 981 */ 982 if (x == width - 1 && y == height - 1) continue; 983 if (!predicate(ch)) { 984 nc.beep(); 985 continue; 986 } 987 if (ch == '\t') ch = ' '; /* This greatly simplifies deleting 988 characters. */ 989 if (echoChars) addch(ch); 990 x = curX; 991 y = curY; 992 res ~= text(ch); 993 } /* while true */ 994 } /* getstr */ 995 996 chtype[] inchstr() 997 { 998 import std.algorithm; 999 import std.array; 1000 1001 auto buffer = new chtype[Curses.lines * Curses.cols + 1]; 1002 buffer[$ - 1] = 0; 1003 winchstr(ptr, buffer.ptr); 1004 return buffer.until(0).array.dup; 1005 } 1006 1007 chtype[] inchstr(int n) 1008 { 1009 import std.algorithm; 1010 import std.array; 1011 1012 auto buffer = new chtype[n + 1]; 1013 buffer[$ - 1] = 0; 1014 winchnstr(ptr, buffer.ptr, n); 1015 return buffer.until(0).array.dup; 1016 } 1017 1018 chtype[] inchstr(int y, int x) 1019 { 1020 move(y, x); 1021 return inchstr; 1022 } 1023 1024 chtype[] inchstr(int y, int x, int n) 1025 { 1026 move(y, x); 1027 return inchstr(n); 1028 } 1029 } 1030 1031 final class ColorTable 1032 { 1033 private: 1034 struct Pair { short fg; short bg; } 1035 1036 Pair[short] pairs; /* A mapping from pair indices to pairs. */ 1037 short[] reusablePairs; 1038 short latestPair = 1; 1039 1040 short latestColor; 1041 short[] reusableColors; 1042 1043 public: 1044 1045 this(bool useStdColors) 1046 { 1047 if (useStdColors) { 1048 latestColor = StdColor.max + 1; 1049 initDefaultColors; 1050 } else { 1051 latestColor = 1; 1052 } 1053 } 1054 1055 /* Indexing a color table returns an attribute which a color pair 1056 represents. */ 1057 chtype opIndex(short fg, short bg) 1058 { 1059 auto pair = Pair(fg, bg); 1060 foreach (index, p; pairs) 1061 if (p == pair) return COLOR_PAIR(index); 1062 throw new NCException("Combination of colors %s:%s is not in the color table"); 1063 } 1064 1065 /* Alternatively, you can use a pair index to get an attribute. */ 1066 chtype opIndex(short pairIndex) 1067 { 1068 if (pairIndex in pairs) 1069 return COLOR_PAIR(pairIndex); 1070 else 1071 throw new NCException("Color pair #%s is not in the color table", pairIndex); 1072 } 1073 1074 /* Return the index of a newly created pair. */ 1075 short addPair(short fg, short bg) 1076 { 1077 bool addNew = reusablePairs == []; 1078 short pair; 1079 if (addNew) 1080 pair = latestPair; 1081 else 1082 pair = reusablePairs[0]; 1083 if (init_pair(pair, fg, bg) != OK) 1084 throw new NCException("Failed to initialize color pair %s:%s", fg, bg); 1085 1086 auto p = Pair(fg, bg); 1087 pairs[pair] = p; 1088 if (addNew) 1089 latestPair++; 1090 else 1091 reusablePairs = reusablePairs[1 .. $]; 1092 return pair; 1093 } 1094 1095 /* Redefine a color. */ 1096 void redefineColor(short color, short r, short g, short b) 1097 { 1098 if (color >= Curses.maxColors) 1099 throw new NCException("A color with index %s requested, but the " ~ 1100 "terminal supports only %s colors", color, Curses.maxColors); 1101 if (init_color(color, r, g, b) != OK) 1102 throw new NCException("Failed to initialize a color #%s with RGB content " ~ 1103 "%s:%s:%s", color, r, g, b); 1104 } 1105 1106 /* Return the index of a newly defined color. */ 1107 short addColor(short r, short g, short b) 1108 { 1109 if (!Curses.canChangeColor) 1110 throw new NCException("The terminal doesn't support changing colors"); 1111 1112 bool addNew = reusableColors == []; 1113 short color; 1114 if (addNew) 1115 color = latestColor; 1116 else 1117 color = reusableColors[0]; 1118 redefineColor(color, r, g, b); 1119 if (addNew) 1120 latestColor++; 1121 else 1122 reusableColors = reusableColors[1 .. $]; 1123 return color; 1124 } 1125 1126 /* Ditto. */ 1127 short addColor(RGB color) 1128 { 1129 return addColor(color.r, color.g, color.b); 1130 } 1131 1132 /* Mark a pair as available for overwriting. It doesn't actually 1133 undefine it or anything, there's no way to do that. */ 1134 void removePair(short pairIndex) 1135 { 1136 if (pairIndex !in pairs) 1137 throw new NCException("Attempted to remove color pair #%s, which is " ~ 1138 "not in the color table to begin with", pairIndex); 1139 reusablePairs ~= pairIndex; 1140 pairs.remove(pairIndex); 1141 } 1142 1143 /* Mark a color as available for overwriting. */ 1144 void removeColor(short color) 1145 { 1146 reusableColors ~= color; 1147 } 1148 1149 void initDefaultColors() 1150 { 1151 import std.traits; 1152 foreach (colorA; EnumMembers!StdColor) 1153 foreach (colorB; EnumMembers!StdColor) 1154 addPair(colorA, colorB); 1155 } 1156 } 1157 1158 /* An exception that is thrown on ncurses errors. */ 1159 class NCException: Exception 1160 { 1161 this(Arg...)(string formatString, Arg args) 1162 { 1163 import std.format; 1164 super(format(formatString, args)); 1165 } 1166 } 1167 1168 /* ---------- helpers and enums ---------- */ 1169 1170 /* Wide character - the return type of getwch function. */ 1171 struct WChar 1172 { 1173 private: 1174 bool isSpecialKey_; 1175 int key_; 1176 wint_t chr_; 1177 1178 public: 1179 bool isSpecialKey() const @property { return isSpecialKey_; } 1180 int key() const @property { return key_; } 1181 wint_t chr() const @property { return chr_; } 1182 1183 bool opBinary(op)(WChar other) 1184 if (op == "==") 1185 { 1186 if (isSpecialKey_ != other.isSpecialKey_) return false; 1187 if (isSpecialKey_) 1188 return key_ == other.key_; 1189 else 1190 return chr_ == other.chr_; 1191 } 1192 1193 this(wint_t key, bool isSpecial = false) 1194 { 1195 isSpecialKey_ = isSpecial; 1196 if (isSpecial) 1197 key_ = key; 1198 else 1199 chr_ = key; 1200 } 1201 1202 this(Key key) 1203 { 1204 this(cast(wint_t)key, true); 1205 } 1206 } 1207 1208 /* Complex character (that is, with attribute bundled) - used as input by some 1209 functions. 1210 */ 1211 struct CChar 1212 { 1213 wint_t[] chars; 1214 chtype attr; 1215 1216 this(wint_t chr, chtype attr = Attr.normal) 1217 { 1218 chars = [chr]; 1219 this.attr = attr; 1220 } 1221 1222 this(const wint_t[] chars, chtype attr = Attr.normal) 1223 { 1224 this.chars = chars.dup; 1225 this.attr = attr; 1226 } 1227 1228 this(const string chars, chtype attr = Attr.normal) 1229 { 1230 import std.conv; 1231 1232 this.chars = chars.to!(wint_t[]); 1233 this.attr = attr; 1234 } 1235 1236 bool opBinary(op)(wint_t chr) 1237 if (op == "==") 1238 { 1239 return chars[0] == chr; 1240 } 1241 1242 alias cchar this; 1243 1244 cchar_t cchar() const @property 1245 { 1246 return prepChar(chars, attr); 1247 } 1248 } 1249 1250 struct RGB 1251 { 1252 short r, g, b; 1253 } 1254 1255 /* Out-of-bounds condition handling. */ 1256 enum OOB 1257 { 1258 ignore, 1259 except 1260 } 1261 1262 enum Attr: chtype 1263 { 1264 normal = A_NORMAL, 1265 charText = A_CHARTEXT, 1266 color = A_COLOR, 1267 standout = A_STANDOUT, 1268 underline = A_UNDERLINE, 1269 reverse = A_REVERSE, 1270 blink = A_BLINK, 1271 dim = A_DIM, 1272 bold = A_BOLD, 1273 altCharSet = A_ALTCHARSET, 1274 invis = A_INVIS, 1275 protect = A_PROTECT, 1276 horizontal = A_HORIZONTAL, 1277 left = A_LEFT, 1278 low = A_LOW, 1279 right = A_RIGHT, 1280 top = A_TOP, 1281 vertical = A_VERTICAL, 1282 } 1283 1284 enum StdColor: short 1285 { 1286 black = COLOR_BLACK, 1287 red = COLOR_RED, 1288 green = COLOR_GREEN, 1289 yellow = COLOR_YELLOW, 1290 blue = COLOR_BLUE, 1291 magenta = COLOR_MAGENTA, 1292 cyan = COLOR_CYAN, 1293 white = COLOR_WHITE, 1294 } 1295 1296 enum Key: int 1297 { 1298 codeYes = KEY_CODE_YES, 1299 min = KEY_MIN, 1300 codeBreak = KEY_BREAK, /* This should've been just 'break', but that's a 1301 keyword. */ 1302 1303 down = KEY_DOWN, 1304 up = KEY_UP, 1305 left = KEY_LEFT, 1306 right = KEY_RIGHT, 1307 home = KEY_HOME, 1308 backspace = KEY_BACKSPACE, 1309 f0 = KEY_F0, 1310 f1 = KEY_F(1), 1311 f2 = KEY_F(2), 1312 f3 = KEY_F(3), 1313 f4 = KEY_F(4), 1314 f5 = KEY_F(5), 1315 f6 = KEY_F(6), 1316 f7 = KEY_F(7), 1317 f8 = KEY_F(8), 1318 f9 = KEY_F(9), 1319 f10 = KEY_F(10), 1320 f11 = KEY_F(11), 1321 f12 = KEY_F(12), 1322 f13 = KEY_F(13), 1323 f14 = KEY_F(14), 1324 f15 = KEY_F(15), 1325 f16 = KEY_F(16), 1326 f17 = KEY_F(17), 1327 f18 = KEY_F(18), 1328 f19 = KEY_F(19), 1329 f20 = KEY_F(20), 1330 f21 = KEY_F(21), 1331 f22 = KEY_F(22), 1332 f23 = KEY_F(23), 1333 f24 = KEY_F(24), 1334 f25 = KEY_F(25), 1335 f26 = KEY_F(26), 1336 f27 = KEY_F(27), 1337 f28 = KEY_F(28), 1338 f29 = KEY_F(29), 1339 f30 = KEY_F(30), 1340 f31 = KEY_F(31), 1341 f32 = KEY_F(32), 1342 f33 = KEY_F(33), 1343 f34 = KEY_F(34), 1344 f35 = KEY_F(35), 1345 f36 = KEY_F(36), 1346 f37 = KEY_F(37), 1347 f38 = KEY_F(38), 1348 f39 = KEY_F(39), 1349 f40 = KEY_F(40), 1350 f41 = KEY_F(41), 1351 f42 = KEY_F(42), 1352 f43 = KEY_F(43), 1353 f44 = KEY_F(44), 1354 f45 = KEY_F(45), 1355 f46 = KEY_F(46), 1356 f47 = KEY_F(47), 1357 f48 = KEY_F(48), 1358 f49 = KEY_F(49), 1359 f50 = KEY_F(50), 1360 f51 = KEY_F(51), 1361 f52 = KEY_F(52), 1362 f53 = KEY_F(53), 1363 f54 = KEY_F(54), 1364 f55 = KEY_F(55), 1365 f56 = KEY_F(56), 1366 f57 = KEY_F(57), 1367 f58 = KEY_F(58), 1368 f59 = KEY_F(59), 1369 f60 = KEY_F(60), 1370 f61 = KEY_F(61), 1371 f62 = KEY_F(62), 1372 f63 = KEY_F(63), 1373 dl = KEY_DL, 1374 il = KEY_IL, 1375 dc = KEY_DC, 1376 ic = KEY_IC, 1377 eic = KEY_EIC, 1378 clear = KEY_CLEAR, 1379 eos = KEY_EOS, 1380 eol = KEY_EOL, 1381 sf = KEY_SF, 1382 sr = KEY_SR, 1383 npage = KEY_NPAGE, 1384 ppage = KEY_PPAGE, 1385 stab = KEY_STAB, 1386 ctab = KEY_CTAB, 1387 catab = KEY_CATAB, 1388 enter = KEY_ENTER, 1389 sreset = KEY_SRESET, 1390 reset = KEY_RESET, 1391 print = KEY_PRINT, 1392 ll = KEY_LL, 1393 a1 = KEY_A1, 1394 a3 = KEY_A3, 1395 b2 = KEY_B2, 1396 c1 = KEY_C1, 1397 c3 = KEY_C3, 1398 btab = KEY_BTAB, 1399 beg = KEY_BEG, 1400 cancel = KEY_CANCEL, 1401 close = KEY_CLOSE, 1402 command = KEY_COMMAND, 1403 copy = KEY_COPY, 1404 create = KEY_CREATE, 1405 end = KEY_END, 1406 exit = KEY_EXIT, 1407 find = KEY_FIND, 1408 help = KEY_HELP, 1409 mark = KEY_MARK, 1410 message = KEY_MESSAGE, 1411 move = KEY_MOVE, 1412 next = KEY_NEXT, 1413 open = KEY_OPEN, 1414 options = KEY_OPTIONS, 1415 previous = KEY_PREVIOUS, 1416 redo = KEY_REDO, 1417 reference = KEY_REFERENCE, 1418 refresh = KEY_REFRESH, 1419 replace = KEY_REPLACE, 1420 restart = KEY_RESTART, 1421 resume = KEY_RESUME, 1422 save = KEY_SAVE, 1423 sbeg = KEY_SBEG, 1424 scancel = KEY_SCANCEL, 1425 scommand = KEY_SCOMMAND, 1426 scopy = KEY_SCOPY, 1427 screate = KEY_SCREATE, 1428 sdc = KEY_SDC, 1429 sdl = KEY_SDL, 1430 select = KEY_SELECT, 1431 send = KEY_SEND, 1432 seol = KEY_SEOL, 1433 sexit = KEY_SEXIT, 1434 sfind = KEY_SFIND, 1435 shelp = KEY_SHELP, 1436 shome = KEY_SHOME, 1437 sic = KEY_SIC, 1438 sleft = KEY_SLEFT, 1439 smessage = KEY_SMESSAGE, 1440 smove = KEY_SMOVE, 1441 snext = KEY_SNEXT, 1442 soptions = KEY_SOPTIONS, 1443 sprevious = KEY_SPREVIOUS, 1444 sprint = KEY_SPRINT, 1445 sredo = KEY_SREDO, 1446 sreplace = KEY_SREPLACE, 1447 sright = KEY_SRIGHT, 1448 srsume = KEY_SRSUME, 1449 ssave = KEY_SSAVE, 1450 ssuspend = KEY_SSUSPEND, 1451 sundo = KEY_SUNDO, 1452 suspend = KEY_SUSPEND, 1453 undo = KEY_UNDO, 1454 mouse = KEY_MOUSE, 1455 resize = KEY_RESIZE, 1456 event = KEY_EVENT, 1457 max = KEY_MAX, 1458 } 1459 1460 enum Align 1461 { 1462 left, 1463 center, 1464 right 1465 } 1466 1467 /* ---------- private helpers ---------- */ 1468 1469 private: 1470 1471 /* Prepare a wide character for drawing. */ 1472 cchar_t 1473 prepChar(C: wint_t, A: chtype)(C ch, A attr) 1474 { 1475 import core.stdc.stddef: wchar_t; 1476 1477 cchar_t res; 1478 wchar_t[] str = [ch, 0]; 1479 setcchar(&res, str.ptr, attr, PAIR_NUMBER(attr), null); 1480 return res; 1481 } 1482 1483 cchar_t 1484 prepChar(C: wint_t, A: chtype)(const C[] chars, A attr) 1485 { 1486 import core.stdc.stddef: wchar_t; 1487 import std.array; 1488 import std.range; 1489 1490 cchar_t res; 1491 version(Win32) { 1492 import std.conv: wtext; 1493 const wchar_t[] str = (chars.take(CCHARW_MAX).wtext) ~ 0; 1494 } else { 1495 const wchar_t[] str = (chars.take(CCHARW_MAX).array) ~ 0; 1496 } 1497 /* Hmm, 'const' modifiers apparently were lost during porting the library 1498 from C to D. 1499 */ 1500 setcchar(&res, cast(wchar_t*) str.ptr, attr, PAIR_NUMBER(attr), null); 1501 return res; 1502 } 1503 1504 /* Advance the cursor by a given amount of spaces. */ 1505 void 1506 advance(ref int y, ref int x, int w, int h, int by) 1507 { 1508 x += by; 1509 while(x >= w) { 1510 x -= w; 1511 y++; 1512 } 1513 } 1514 1515 CChar 1516 fromGrapheme(Grapheme g, chtype attr = Attr.normal) 1517 { 1518 import std.array; 1519 1520 version(Win32) { 1521 import std.conv: wtext; 1522 return CChar(g[].wtext, attr); 1523 } else { 1524 return CChar(g[].array, attr); 1525 } 1526 } 1527 1528 /* Returns visual lenght of a string. */ 1529 int 1530 lineLength(string str) 1531 { 1532 import std.array; 1533 import std.uni; 1534 1535 int res = 0; 1536 foreach (gr; str.byGrapheme) { 1537 if (gr[0] == '\t') 1538 res += 8; 1539 else 1540 res++; 1541 } 1542 return res; 1543 }