1 module tui; 2 3 import colored : forceStyle, Style; 4 import core.sys.posix.signal : SIGINT; 5 import core.sys.posix.sys.ioctl : ioctl, TIOCGWINSZ, winsize; 6 import core.sys.posix.termios : ECHO, ICANON, tcgetattr, TCSAFLUSH, TCSANOW, tcsetattr, termios; 7 import std.algorithm : countUntil, find, max, min; 8 import std.array : appender, array; 9 import std.conv : to; 10 import std.exception : enforce, errnoEnforce; 11 import std.math.algebraic : abs; 12 import std.range : cycle, empty, front, popFront; 13 import std.signals; 14 import std.string : format, join, split; 15 import std.typecons : Tuple; 16 17 version (unittest) 18 { 19 import unit_threaded; 20 } 21 22 alias Position = Tuple!(int, "x", int, "y"); 23 alias Dimension = Tuple!(int, "width", int, "height"); /// https://en.wikipedia.org/wiki/ANSI_escape_code 24 enum SIGWINCH = 28; 25 @safe auto next(Range)(Range r) 26 { 27 r.popFront; 28 return r.front; 29 } 30 31 @("next") unittest 32 { 33 auto range = [1, 2, 3]; 34 range.next.should == 2; 35 range.next.should == 2; 36 } 37 38 enum Operation : string 39 { 40 CURSOR_UP = "A", 41 CURSOR_DOWN = "B", 42 CURSOR_FORWARD = "C", 43 CURSOR_BACKWARD = "D", 44 CURSOR_POSITION = "H", 45 ERASE_IN_DISPLAY = "J", 46 ERASE_IN_LINE = "K", 47 DEVICE_STATUS_REPORT = "n", 48 CURSOR_POSITION_REPORT = "R", 49 CLEAR_TERMINAL = "2J", 50 CLEAR_LINE = "2K", 51 } 52 53 enum State : string 54 { 55 CURSOR = "?25", 56 ALTERNATE_BUFFER = "?1049", 57 } 58 59 enum Mode : string 60 { 61 LOW = "l", 62 HIGH = "h", 63 } 64 65 string execute(Operation operation, string[] args...) 66 { 67 return "\x1b[" ~ args.join(";") ~ operation; 68 } 69 70 string to(State state, Mode mode) 71 { 72 return "\x1b[" ~ state ~ mode; 73 } 74 75 __gshared Terminal INSTANCE; 76 77 extern (C) void signal(int sig, void function(int)); 78 extern (C) void ctrlC(int s) 79 { 80 import core.sys.posix.unistd : write; 81 82 INSTANCE.ctrlCSignalFD().write(&s, s.sizeof); 83 } 84 85 class SelectSet 86 { 87 import core.stdc.errno : EINTR, errno; 88 import core.sys.posix.sys.select : FD_ISSET, FD_SET, fd_set, FD_ZERO, select; 89 90 fd_set fds; 91 int maxFD; 92 this() 93 { 94 FD_ZERO(&fds); 95 maxFD = 0; 96 } 97 98 void addFD(int fd) 99 { 100 FD_SET(fd, &fds); 101 maxFD = max(fd, maxFD); 102 } 103 104 int readyForRead() 105 { 106 return select(maxFD + 1, &fds, null, null, null); 107 } 108 109 bool isSet(int fd) 110 { 111 return FD_ISSET(fd, &fds); 112 } 113 } 114 115 class Terminal 116 { 117 int stdinFD; 118 int stdoutFD; 119 termios originalState; 120 auto buffer = appender!(char[])(); 121 /// used to handle signals 122 int[2] selfSignalFDs; 123 int ctrlCSignalFD() 124 { 125 return selfSignalFDs[1]; 126 } 127 128 /// used to run delegates in the input handling thread 129 int[2] terminalThreadFDs; 130 void delegate()[] terminalThreadDelegates; 131 132 this(int stdinFD = 0, int stdoutFD = 1) 133 { 134 import core.sys.posix.unistd : pipe; 135 136 auto result = pipe(this.selfSignalFDs); 137 (result != -1).errnoEnforce("Cannot create pipe for signal handling"); 138 139 result = pipe(this.terminalThreadFDs); 140 (result != -1).errnoEnforce("Cannot create pipe for run in terminal input thread"); 141 import std.stdio : writeln; 142 143 writeln("terminalthreadfds: ", terminalThreadFDs); 144 import std.stdio : writeln; 145 146 writeln("selfSignalFDs: ", selfSignalFDs); 147 148 this.stdinFD = stdinFD; 149 this.stdoutFD = stdoutFD; 150 writeln(format("in out [%s, %s]", stdinFD, stdoutFD)); 151 152 (tcgetattr(stdoutFD, &originalState) == 0).errnoEnforce("Cannot get termios"); 153 154 termios newState = originalState; 155 newState.c_lflag &= ~ECHO & ~ICANON; 156 (tcsetattr(stdoutFD, TCSAFLUSH, &newState) == 0).errnoEnforce( 157 "Cannot set new termios state"); 158 159 wDirect(State.ALTERNATE_BUFFER.to(Mode.HIGH), "Cannot switch to alternate buffer"); 160 wDirect(Operation.CLEAR_TERMINAL.execute, "Cannot clear terminal"); 161 wDirect(State.CURSOR.to(Mode.LOW), "Cannot hide cursor"); 162 163 INSTANCE = this; 164 2.signal(&ctrlC); 165 } 166 167 ~this() 168 { 169 wDirect(Operation.CLEAR_TERMINAL.execute, "Cannot clear alternate buffer"); 170 wDirect(State.ALTERNATE_BUFFER.to(Mode.LOW), "Cannot switch to normal buffer"); 171 wDirect(State.CURSOR.to(Mode.HIGH), "Cannot show cursor"); 172 173 (tcsetattr(stdoutFD, TCSANOW, &originalState) == 0).errnoEnforce( 174 "Cannot set original termios state"); 175 import core.sys.posix.unistd : close; 176 177 selfSignalFDs[0].close(); 178 selfSignalFDs[1].close(); 179 180 terminalThreadFDs[0].close(); 181 terminalThreadFDs[1].close(); 182 } 183 184 auto putString(string s) 185 { 186 w(s); 187 return this; 188 } 189 190 auto xy(int x, int y) 191 { 192 w(Operation.CURSOR_POSITION.execute((y + 1).to!string, (x + 1).to!string)); 193 return this; 194 } 195 196 final void wDirect(string data, lazy string errorMessage) 197 { 198 import core.sys.posix.unistd : write; 199 200 (2.write(data.ptr, data.length) == data.length).errnoEnforce(errorMessage); 201 } 202 203 final void w(string data) 204 { 205 buffer.put(cast(char[]) data); 206 } 207 208 auto clearBuffer() 209 { 210 buffer.clear; 211 w(Operation.CLEAR_TERMINAL.execute); 212 return this; 213 } 214 215 auto flip() 216 { 217 auto data = buffer.data; 218 // was 2 ??? 219 import core.sys.posix.unistd : write; 220 221 (2.write(data.ptr, data.length) == data.length).errnoEnforce("Cannot blit data"); 222 return this; 223 } 224 225 Dimension dimension() 226 { 227 winsize ws; 228 (ioctl(stdoutFD, TIOCGWINSZ, &ws) == 0).errnoEnforce("Cannot get winsize"); 229 return Dimension(ws.ws_col, ws.ws_row); 230 } 231 232 void runInTerminalThread(void delegate() d) 233 { 234 synchronized (this) 235 { 236 terminalThreadDelegates ~= d; 237 } 238 ubyte h = 0; 239 import core.sys.posix.unistd : write; 240 241 (terminalThreadFDs[1].write(&h, h.sizeof) == h.sizeof).errnoEnforce( 242 "Cannot write ubyte to terminalThreadFD"); 243 } 244 245 immutable(KeyInput) getInput() 246 { 247 // osx needs to do select when working with /dev/tty https://nathancraddock.com/blog/macos-dev-tty-polling/ 248 scope select = new SelectSet(); 249 select.addFD(selfSignalFDs[0]); 250 select.addFD(terminalThreadFDs[0]); 251 select.addFD(stdinFD); 252 253 int result = select.readyForRead(); 254 if (result == -1) 255 { 256 return KeyInput.fromInterrupt(); 257 } 258 else if (result > 0) 259 { 260 if (select.isSet(selfSignalFDs[0])) 261 { 262 import core.sys.posix.unistd : read; 263 264 int buffer; 265 auto count = selfSignalFDs[0].read(&buffer, buffer.sizeof); 266 (count == buffer.sizeof).errnoEnforce("Cannot read on self signal fds read end"); 267 268 return KeyInput.fromCtrlC(); 269 } 270 else if (select.isSet(terminalThreadFDs[0])) 271 { 272 ubyte buffer; 273 import core.sys.posix.unistd : read; 274 275 auto count = read(terminalThreadFDs[0], &buffer, buffer.sizeof); 276 (count == buffer.sizeof).errnoEnforce(format("Cannot read next delegate on fd %s", 277 terminalThreadFDs[0])); 278 279 if (terminalThreadDelegates.length > 0) 280 { 281 auto h = terminalThreadDelegates[0]; 282 terminalThreadDelegates = terminalThreadDelegates[1 .. $]; 283 h(); 284 } 285 // do not return but process key inputs 286 } 287 288 if (select.isSet(stdinFD)) 289 { 290 import core.sys.posix.unistd : read; 291 292 char[32] buffer; 293 auto count = stdinFD.read(&buffer, buffer.length); 294 (count != -1).errnoEnforce("Cannot read next input"); 295 296 return KeyInput.fromText(buffer[0 .. count].idup); 297 } 298 299 } 300 return KeyInput.fromEmpty(); 301 } 302 } 303 304 enum Key : string 305 { 306 up = [27, 91, 65], 307 down = [27, 91, 66], 308 left = [27, 91, 67], 309 right = [27, 91, 68], /+ 310 311 codeYes = KEY_CODE_YES, 312 min = KEY_MIN, 313 codeBreak = KEY_BREAK, 314 left = KEY_LEFT, 315 right = KEY_RIGHT, 316 home = KEY_HOME, 317 backspace = KEY_BACKSPACE, 318 f0 = KEY_F0, 319 f1 = KEY_F(1), 320 f2 = KEY_F(2), 321 f3 = KEY_F(3), 322 f4 = KEY_F(4), 323 f5 = KEY_F(5), 324 f6 = KEY_F(6), 325 f7 = KEY_F(7), 326 f8 = KEY_F(8), 327 f9 = KEY_F(9), 328 f10 = KEY_F(10), 329 f11 = KEY_F(11), 330 f12 = KEY_F(12), 331 f13 = KEY_F(13), 332 f14 = KEY_F(14), 333 f15 = KEY_F(15), 334 f16 = KEY_F(16), 335 f17 = KEY_F(17), 336 f18 = KEY_F(18), 337 f19s = KEY_F(19), 338 f20 = KEY_F(20), 339 f21 = KEY_F(21), 340 f22 = KEY_F(22), 341 f23 = KEY_F(23), 342 f24 = KEY_F(24), 343 f25 = KEY_F(25), 344 f26 = KEY_F(26),| 345 f27 = KEY_F(27), 346 f28 = KEY_F(28), 347 f29 = KEY_F(29), 348 f30 = KEY_F(30), 349 f31 = KEY_F(31), 350 f32 = KEY_F(32), 351 f33 = KEY_F(33), 352 f34 = KEY_F(34), 353 f35 = KEY_F(35), 354 f36 = KEY_F(36), 355 f37 = KEY_F(37), 356 f38 = KEY_F(38), 357 f39 = KEY_F(39), 358 f40 = KEY_F(40), 359 f41 = KEY_F(41), 360 f42 = KEY_F(42), 361 f43 = KEY_F(43), 362 f44 = KEY_F(44), 363 f45 = KEY_F(45), 364 f46 = KEY_F(46), 365 f47 = KEY_F(47), 366 f48 = KEY_F(48), 367 f49 = KEY_F(49), 368 f50 = KEY_F(50), 369 f51 = KEY_F(51), 370 f52 = KEY_F(52), 371 f53 = KEY_F(53), 372 f54 = KEY_F(54), 373 f55 = KEY_F(55), 374 f56 = KEY_F(56), 375 f57 = KEY_F(57), 376 f58 = KEY_F(58), 377 f59 = KEY_F(59), 378 f60 = KEY_F(60), 379 f61 = KEY_F(61), 380 f62 = KEY_F(62), 381 f63 = KEY_F(63), 382 dl = KEY_DL, 383 il = KEY_IL, 384 dc = KEY_DC, 385 ic = KEY_IC, 386 eic = KEY_EIC, 387 clear = KEY_CLEAR, 388 eos = KEY_EOS, 389 eol = KEY_EOL, 390 sf = KEY_SF, 391 sr = KEY_SR, 392 npage = KEY_NPAGE, 393 ppage = KEY_PPAGE, 394 stab = KEY_STAB, 395 ctab = KEY_CTAB, 396 catab = KEY_CATAB, 397 enter = KEY_ENTER, 398 sreset = KEY_SRESET, 399 reset = KEY_RESET, 400 print = KEY_PRINT, 401 ll = KEY_LL, 402 a1 = KEY_A1, 403 a3 = KEY_A3, 404 b2 = KEY_B2, 405 c1 = KEY_C1, 406 c3 = KEY_C3, 407 btab = KEY_BTAB, 408 beg = KEY_BEG, 409 cancel = KEY_CANCEL, 410 close = KEY_CLOSE, 411 command = KEY_COMMAND, 412 copy = KEY_COPY, 413 create = KEY_CREATE, 414 end = KEY_END, 415 exit = KEY_EXIT, 416 find = KEY_FIND, 417 help = KEY_HELP, 418 mark = KEY_MARK, 419 message = KEY_MESSAGE, 420 move = KEY_MOVE, 421 next = KEY_NEXT, 422 open = KEY_OPEN, 423 options = KEY_OPTIONS, 424 previous = KEY_PREVIOUS, 425 redo = KEY_REDO, 426 reference = KEY_REFERENCE, 427 refresh = KEY_REFRESH, 428 replace = KEY_REPLACE, 429 restart = KEY_RESTART, 430 resume = KEY_RESUME, 431 save = KEY_SAVE, 432 sbeg = KEY_SBEG, 433 scancel = KEY_SCANCEL, 434 scommand = KEY_SCOMMAND, 435 scopy = KEY_SCOPY, 436 screate = KEY_SCREATE, 437 sdc = KEY_SDC, 438 sdl = KEY_SDL, 439 select = KEY_SELECT, 440 send = KEY_SEND, 441 seol = KEY_SEOL, 442 sexit = KEY_SEXIT, 443 sfind = KEY_SFIND, 444 shelp = KEY_SHELP, 445 shome = KEY_SHOME, 446 sic = KEY_SIC, 447 sleft = KEY_SLEFT, 448 smessage = KEY_SMESSAGE, 449 smove = KEY_SMOVE, 450 snext = KEY_SNEXT, 451 soptions = KEY_SOPTIONS, 452 sprevious = KEY_SPREVIOUS, 453 sprint = KEY_SPRINT, 454 sredo = KEY_SREDO, 455 sreplace = KEY_SREPLACE, 456 sright = KEY_SRIGHT, 457 srsume = KEY_SRSUME, 458 ssave = KEY_SSAVE, 459 ssuspend = KEY_SSUSPEND, 460 sundo = KEY_SUNDO, 461 suspend = KEY_SUSPEND, 462 undo = KEY_UNDO, 463 mouse = KEY_MOUSE, 464 resize = KEY_RESIZE, 465 event = KEY_EVENT, 466 max = KEY_MAX, 467 +/ 468 469 470 471 } 472 /+ 473 enum Attributes : chtype 474 { 475 normal = A_NORMAL, 476 charText = A_CHARTEXT, 477 color = A_COLOR, 478 standout = A_STANDOUT, 479 underline = A_UNDERLINE, 480 reverse = A_REVERSE, 481 blink = A_BLINK, 482 dim = A_DIM, 483 bold = A_BOLD, 484 altCharSet = A_ALTCHARSET, 485 invis = A_INVIS, 486 protect = A_PROTECT, 487 horizontal = A_HORIZONTAL, 488 left = A_LEFT, 489 low = A_LOW, 490 right = A_RIGHT, 491 top = A_TOP, 492 vertical = A_VERTICAL, 493 } 494 495 +/ 496 /// either a special key like arrow or backspace 497 /// or an utf-8 string (e.g. ä is already 2 bytes in an utf-8 string) 498 struct KeyInput 499 { 500 static int COUNT = 0; 501 int count; 502 string input; 503 byte[] bytes; 504 bool ctrlC; 505 bool empty; 506 this(string input) 507 { 508 this.count = COUNT++; 509 this.input = input.dup; 510 this.ctrlC = false; 511 this.empty = false; 512 } 513 514 this(byte[] bytes) 515 { 516 this.count = COUNT++; 517 this.bytes = bytes; 518 this.ctrlC = false; 519 this.empty = false; 520 } 521 522 this(bool ctrlC, bool empty) 523 { 524 this.count = COUNT++; 525 this.bytes = null; 526 this.ctrlC = ctrlC; 527 this.empty = empty; 528 } 529 530 static auto fromCtrlC() 531 { 532 return cast(immutable) KeyInput(true, false); 533 } 534 535 static auto fromInterrupt() 536 { 537 return cast(immutable) KeyInput(false, true); 538 } 539 540 static auto fromText(string s) 541 { 542 return cast(immutable) KeyInput(s); 543 } 544 545 static auto fromBytes(byte[] bytes) 546 { 547 return KeyInput(bytes); 548 } 549 550 static auto fromEmpty() 551 { 552 return cast(immutable) KeyInput(false, true); 553 } 554 } 555 556 class NoKeyException : Exception 557 { 558 this(string s) 559 { 560 super(s); 561 } 562 } 563 564 int byteCount(int k) 565 { 566 if (k < 0b1100_0000) 567 { 568 return 1; 569 } 570 if (k < 0b1110_0000) 571 { 572 return 2; 573 } 574 575 if (k > 0b1111_0000) 576 { 577 return 3; 578 } 579 580 return 4; 581 } 582 583 alias InputHandler = bool delegate(KeyInput input); 584 alias ButtonHandler = void delegate(); 585 abstract class Component 586 { 587 Component parent; 588 Component[] children; 589 590 // the root of a component hierarchy carries all focusComponents, 591 // atm those have to be registered manually via 592 // addToFocusComponents. 593 Component focusPath; 594 595 // component that is really focused atm 596 Component currentFocusedComponent; 597 // stores the focused component in case a popup is pushed 598 Component lastFocusedComponent; 599 600 InputHandler inputHandler; 601 602 int left; /// Left position of the component relative to the parent 603 int top; /// Top position of the component relative to the parent 604 int width; /// Width of the component 605 int height; /// Height of the component 606 607 this(Component[] children = null) 608 { 609 this.children = children; 610 foreach (child; children) 611 { 612 child.setParent(this); 613 } 614 } 615 616 void clearFocus() 617 { 618 this.currentFocusedComponent = null; 619 foreach (Component c; children) 620 { 621 c.clearFocus(); 622 } 623 } 624 625 void setInputHandler(InputHandler inputHandler) 626 { 627 this.inputHandler = inputHandler; 628 } 629 630 void resize(int left, int top, int width, int height) 631 { 632 this.left = left; 633 this.top = top; 634 this.width = width; 635 this.height = height; 636 } 637 638 void setParent(Component parent) 639 { 640 this.parent = parent; 641 } 642 643 abstract void render(Context context); 644 bool handlesInput() 645 { 646 return true; 647 } 648 649 bool focusable() 650 { 651 return false; 652 } 653 654 bool handleInput(KeyInput input) 655 { 656 switch (input.input) 657 { 658 case "\t": 659 focusNext(); 660 return true; 661 default: 662 if (focusPath !is null && focusPath.handleInput(input)) 663 { 664 return true; 665 } 666 if (inputHandler !is null && inputHandler(input)) 667 { 668 return true; 669 } 670 return false; 671 } 672 } 673 // establishes the input handling path from current focused 674 // child to the root component 675 void requestFocus() 676 { 677 currentFocusedComponent = this; 678 if (this.parent !is null) 679 { 680 this.parent.buildFocusPath(this, this); 681 } 682 } 683 684 void buildFocusPath(Component focusedComponent, Component path) 685 { 686 enforce(children.countUntil(path) >= 0, "Cannot find child"); 687 this.focusPath = path; 688 if (this.currentFocusedComponent !is null) 689 { 690 this.currentFocusedComponent.currentFocusedComponent = focusedComponent; 691 } 692 this.currentFocusedComponent = focusedComponent; 693 if (this.parent !is null) 694 { 695 this.parent.buildFocusPath(focusedComponent, this); 696 } 697 } 698 699 void focusNext() 700 { 701 if (parent is null) 702 { 703 auto components = findAllFocusableComponents(); 704 if (components.empty) 705 { 706 return; 707 } 708 if (currentFocusedComponent is null) 709 { 710 components.front.requestFocus; 711 } 712 else 713 { 714 components.cycle.find(currentFocusedComponent).next.requestFocus; 715 } 716 } 717 else 718 { 719 parent.focusNext(); 720 } 721 } 722 723 private Component[] findAllFocusableComponents(Component[] result = null) 724 { 725 if (focusable()) 726 { 727 result ~= this; 728 } 729 foreach (child; children) 730 { 731 result = child.findAllFocusableComponents(result); 732 } 733 return result; 734 } 735 } 736 737 class HSplit : Component 738 { 739 int split; 740 this(int split, Component top, Component bottom) 741 { 742 super([top, bottom]); 743 this.split = split; 744 } 745 746 override void resize(int left, int top, int width, int height) 747 { 748 super.resize(left, top, width, height); 749 750 int splitPos = split; 751 if (split < 0) 752 { 753 splitPos = height + split; 754 } 755 this.top.resize(0, 0, width, splitPos); 756 this.bottom.resize(0, splitPos, width, height - splitPos); 757 } 758 759 override void render(Context context) 760 { 761 this.top.render(context.forChild(this.top)); 762 this.bottom.render(context.forChild(this.bottom)); 763 } 764 765 private Component top() 766 { 767 return children[0]; 768 } 769 770 private Component bottom() 771 { 772 return children[1]; 773 } 774 } 775 776 class VSplit : Component 777 { 778 int split; 779 this(int split, Component left, Component right) 780 { 781 super([left, right]); 782 this.split = split; 783 } 784 785 override void resize(int left, int top, int width, int height) 786 { 787 super.resize(left, top, width, height); 788 789 int splitPos = split; 790 if (split < 0) 791 { 792 splitPos = width + split; 793 } 794 this.left.resize(0, 0, splitPos, height); 795 this.right.resize(splitPos, 0, width - split, height); 796 } 797 798 override void render(Context context) 799 { 800 left.render(context.forChild(left)); 801 right.render(context.forChild(right)); 802 } 803 804 private Component left() 805 { 806 return children[0]; 807 } 808 809 private Component right() 810 { 811 return children[1]; 812 } 813 } 814 815 class Filled : Component 816 { 817 string what; 818 this(string what) 819 { 820 this.what = what; 821 } 822 823 override void render(Context context) 824 { 825 for (int y = 0; y < height; y++) 826 { 827 for (int x = 0; x < width; x++) 828 { 829 context.putString(x, y, what); 830 } 831 } 832 context.putString(0, 0, "0"); 833 context.putString(width - 1, height - 1, "1"); 834 } 835 836 override bool handlesInput() 837 { 838 return false; 839 } 840 } 841 842 class Text : Component 843 { 844 string content; 845 this(string content) 846 { 847 this.content = content; 848 } 849 850 override void render(Context context) 851 { 852 context.putString(0, 0, content); 853 } 854 855 override bool handlesInput() 856 { 857 return false; 858 } 859 } 860 861 class MultilineText : Component 862 { 863 string[] lines; 864 this(string content) 865 { 866 lines = content.split("\n"); 867 } 868 869 override void render(Context context) 870 { 871 foreach (idx, line; lines) 872 { 873 context.putString(0, cast(int) idx, line); 874 } 875 } 876 877 override bool handlesInput() 878 { 879 return false; 880 } 881 } 882 883 class Canvas : Component 884 { 885 class Graphics 886 { 887 import std.uni : unicode; 888 889 static braille = unicode.Braille.byCodepoint.array; 890 int[] pixels; 891 this() 892 { 893 pixels = new int[width * height]; 894 } 895 896 int getWidth() 897 { 898 return width * 2; 899 } 900 901 int getHeight() 902 { 903 return height * 4; 904 } 905 // see https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm#All_cases 906 void line(const(Position) from, const(Position) to) 907 { 908 const int dx = (to.x - from.x).abs; 909 const int stepX = from.x < to.x ? 1 : -1; 910 911 const int dy = -(to.y - from.y).abs; 912 const int stepY = from.y < to.y ? 1 : -1; 913 914 int error = dx + dy; 915 int x = from.x; 916 int y = from.y; 917 while (true) 918 { 919 set(Position(x, y)); 920 if (x == to.x && y == to.y) 921 { 922 break; 923 } 924 const e2 = 2 * error; 925 if (e2 >= dy) 926 { 927 if (x == to.x) 928 { 929 break; 930 } 931 error += dy; 932 x += stepX; 933 } 934 if (e2 <= dx) 935 { 936 if (y == to.y) 937 { 938 break; 939 } 940 error += dx; 941 y += stepY; 942 } 943 } 944 } 945 // x and y in braille coords 946 void set(const(Position) p) 947 { 948 enforce(p.x < getWidth(), "x: %s needs to be smaller than %s".format(p.x, getWidth)); 949 enforce(p.y < getHeight(), "y: %s needs to be smaller than %s".format(p.y, getHeight)); 950 enforce(p.x >= 0, "x: %s needs to be >= 0".format(p.x)); 951 enforce(p.y >= 0, "y: %s needs to be >= 0".format(p.y)); 952 // bit nr 953 // 0 3 954 // 1 4 955 // 2 5 956 // 6 7 957 int xIdx = p.x / 2; 958 int yIdx = p.y / 4; 959 960 int brailleX = p.x % 2; 961 int brailleY = p.y % 4; 962 static brailleBits = [0, 1, 2, 6, 3, 4, 5, 7]; 963 int idx = xIdx + yIdx * width; 964 pixels[idx] |= 1 << brailleBits[brailleY + brailleX * 4]; 965 } 966 967 void render(Context context) 968 { 969 for (int j = 0; j < height; ++j) 970 { 971 for (int i = 0; i < width; ++i) 972 { 973 const idx = i + j * width; 974 const p = pixels[idx]; 975 if (p != 0) 976 { 977 context.putString(i, j, "%s".format(braille[p])); 978 } 979 } 980 } 981 } 982 } 983 984 alias Painter = void delegate(Canvas.Graphics, Context); 985 Painter painter; 986 this(Painter painter) 987 { 988 this.painter = painter; 989 } 990 991 override void render(Context context) 992 { 993 scope g = new Graphics(); 994 painter(g, context); 995 g.render(context); 996 } 997 998 override bool handlesInput() 999 { 1000 return false; 1001 } 1002 } 1003 1004 class Button : Component 1005 { 1006 string text; 1007 ButtonHandler pressed; 1008 1009 this(string text, ButtonHandler pressed) 1010 { 1011 this.text = text; 1012 this.pressed = pressed; 1013 } 1014 1015 override void render(Context c) 1016 { 1017 if (currentFocusedComponent == this) 1018 { 1019 c.putString(0, 0, "> " ~ text); 1020 } 1021 else 1022 { 1023 c.putString(0, 0, " " ~ text); 1024 } 1025 } 1026 1027 override bool handleInput(KeyInput input) 1028 { 1029 switch (input.input) 1030 { 1031 case " ": 1032 pressed(); 1033 return true; 1034 default: 1035 return false; 1036 } 1037 } 1038 1039 override bool focusable() 1040 { 1041 return true; 1042 } 1043 1044 override string toString() 1045 { 1046 return "Button"; 1047 } 1048 } 1049 1050 string dropIgnoreAnsiEscapes(string s, int n) 1051 { 1052 string result; 1053 bool inColorAnsiEscape = false; 1054 int count = 0; 1055 1056 if (n < 0) 1057 { 1058 n = -n; 1059 result = s; 1060 for (int i = 0; i < n; ++i) 1061 { 1062 result = " " ~ result; 1063 } 1064 return result; 1065 } 1066 1067 while (!s.empty) 1068 { 1069 auto current = s.front; 1070 if (current == 27) 1071 { 1072 inColorAnsiEscape = true; 1073 result ~= current; 1074 } 1075 else 1076 { 1077 if (inColorAnsiEscape) 1078 { 1079 if (current == 'm') 1080 { 1081 inColorAnsiEscape = false; 1082 } 1083 result ~= current; 1084 } 1085 else 1086 { 1087 if (count >= n) 1088 { 1089 result ~= current; 1090 } 1091 count++; 1092 } 1093 } 1094 s.popFront; 1095 } 1096 return result; 1097 } 1098 1099 @("dropIgnoreAnsiEscapes/basic") unittest 1100 { 1101 import unit_threaded; 1102 1103 "abc".dropIgnoreAnsiEscapes(1).should == "bc"; 1104 } 1105 1106 @("dropIgnoreAnsiEscapes/basicWithAnsi") unittest 1107 { 1108 import unit_threaded; 1109 1110 "a\033[123mbcdefghijkl".dropIgnoreAnsiEscapes(3).should == "\033[123mdefghijkl"; 1111 } 1112 1113 @("dropIgnoreAnsiEscapes/dropAll") unittest 1114 { 1115 import unit_threaded; 1116 1117 "abc".dropIgnoreAnsiEscapes(4).should == ""; 1118 } 1119 1120 @("dropIgnoreAnsiEscapes/negativeNumber") unittest 1121 { 1122 import unit_threaded; 1123 1124 "abc".dropIgnoreAnsiEscapes(-1).should == " abc"; 1125 } 1126 1127 string takeIgnoreAnsiEscapes(string s, uint length) 1128 { 1129 string result; 1130 uint count = 0; 1131 bool inColorAnsiEscape = false; 1132 while (!s.empty) 1133 { 1134 auto current = s.front; 1135 if (current == 27) 1136 { 1137 inColorAnsiEscape = true; 1138 result ~= current; 1139 } 1140 else 1141 { 1142 if (inColorAnsiEscape) 1143 { 1144 result ~= current; 1145 if (current == 'm') 1146 { 1147 inColorAnsiEscape = false; 1148 } 1149 } 1150 else 1151 { 1152 if (count < length) 1153 { 1154 result ~= current; 1155 count++; 1156 } 1157 } 1158 } 1159 s.popFront; 1160 } 1161 return result; 1162 } 1163 1164 @("takeIgnoreAnsiEscapes") unittest 1165 { 1166 import unit_threaded; 1167 1168 "hello world".takeIgnoreAnsiEscapes(5).should == "hello"; 1169 "he\033[123mllo world\033[0m".takeIgnoreAnsiEscapes(5).should == "he\033[123mllo\033[0m"; 1170 "köstlin".takeIgnoreAnsiEscapes(10).should == "köstlin"; 1171 } 1172 1173 int clipTo(int v, size_t maximum) 1174 { 1175 return min(v, maximum); 1176 } 1177 1178 class List(T, alias stringTransform) : Component 1179 { 1180 T[] model; 1181 T[]delegate() getData; 1182 1183 ScrollInfo scrollInfo; 1184 mixin Signal!(T) selectionChanged; 1185 bool vMirror; 1186 1187 struct ScrollInfo 1188 { 1189 int selection; 1190 int offset; 1191 void up() 1192 { 1193 if (selection > 0) 1194 { 1195 selection--; 1196 while (selection < offset) 1197 { 1198 offset--; 1199 } 1200 } 1201 } 1202 1203 void down(T[] model, int height) 1204 { 1205 if (selection + 1 < model.length) 1206 { 1207 selection++; 1208 while (selection >= offset + height) 1209 { 1210 offset++; 1211 } 1212 } 1213 } 1214 } 1215 1216 this(T[] model, bool vMirror = false) 1217 { 1218 this.model = model; 1219 this.scrollInfo = ScrollInfo(0, 0); 1220 this.vMirror = vMirror; 1221 } 1222 1223 this(T[]delegate() getData, bool vMirror = false) 1224 { 1225 this.getData = getData; 1226 this.scrollInfo = ScrollInfo(0, 0); 1227 this.vMirror = vMirror; 1228 } 1229 1230 override void render(Context context) 1231 { 1232 if (getData) 1233 { 1234 model = getData(); 1235 } 1236 scrollInfo.offset = scrollInfo.offset.clipTo(model.length + -1); 1237 if (model.length + -1 < context.height) 1238 { 1239 scrollInfo.offset = 0; 1240 } 1241 scrollInfo.selection = scrollInfo.selection.clipTo(model.length + -1); 1242 for (int i = 0; i < height; ++i) 1243 { 1244 const index = i + scrollInfo.offset; 1245 if (index >= model.length) 1246 return; 1247 const selected = (index == scrollInfo.selection) && (currentFocusedComponent == this); 1248 auto text = "%s %s".format(selected ? ">" : " ", stringTransform(model[index])); 1249 text = selected ? text.forceStyle(Style.reverse) : text; 1250 context.putString(0, vMirror ? height - 1 - i : i, text); 1251 } 1252 } 1253 1254 void up() 1255 { 1256 vMirror ? _down : _up; 1257 } 1258 1259 void down() 1260 { 1261 vMirror ? _up : _down; 1262 } 1263 1264 void _up() 1265 { 1266 if (model.empty) 1267 { 1268 return; 1269 } 1270 scrollInfo.up; 1271 selectionChanged.emit(model[scrollInfo.selection]); 1272 } 1273 1274 void _down() 1275 { 1276 if (model.empty) 1277 { 1278 return; 1279 } 1280 scrollInfo.down(model, height); 1281 selectionChanged.emit(model[scrollInfo.selection]); 1282 } 1283 1284 void select() 1285 { 1286 if (model.empty) 1287 { 1288 return; 1289 } 1290 selectionChanged.emit(model[scrollInfo.selection]); 1291 } 1292 1293 auto getSelection() 1294 { 1295 return model[scrollInfo.selection]; 1296 } 1297 1298 override bool handleInput(KeyInput input) 1299 { 1300 switch (input.input) 1301 { 1302 case Key.up: 1303 up(); 1304 return true; 1305 case Key.down: 1306 down(); 1307 return true; 1308 default: 1309 return super.handleInput(input); 1310 } 1311 } 1312 1313 override bool focusable() 1314 { 1315 return true; 1316 } 1317 1318 override string toString() 1319 { 1320 return "List"; 1321 } 1322 } 1323 1324 struct Viewport 1325 { 1326 int x; 1327 int y; 1328 int width; 1329 int height; 1330 } 1331 1332 class ScrollPane : Component 1333 { 1334 Viewport viewport; 1335 this(Component child) 1336 { 1337 super([child]); 1338 } 1339 1340 bool up() 1341 { 1342 viewport.y = max(viewport.y - 1, 0); 1343 return true; 1344 } 1345 1346 bool down() 1347 { 1348 viewport.y++; 1349 return true; 1350 } 1351 1352 bool left() 1353 { 1354 viewport.x++; 1355 return true; 1356 } 1357 1358 bool right() 1359 { 1360 viewport.x = max(viewport.x - 1, 0); 1361 return true; 1362 } 1363 1364 override bool handleInput(KeyInput input) 1365 { 1366 switch (input.input) 1367 { 1368 case "w": 1369 case "j": 1370 case Key.up: 1371 return up(); 1372 case "s": 1373 case "k": 1374 case Key.down: 1375 return down(); 1376 case "a": 1377 case "h": 1378 case Key.left: 1379 return left(); 1380 case "d": 1381 case "l": 1382 case Key.right: 1383 return right(); 1384 default: 1385 return super.handleInput(input); 1386 } 1387 } 1388 1389 override bool focusable() 1390 { 1391 return true; 1392 } 1393 1394 override void render(Context c) 1395 { 1396 auto child = children.front; 1397 child.render(c.forChild(child, viewport)); 1398 if (currentFocusedComponent == this) 1399 { 1400 c.putString(0, 0, ">"); 1401 } 1402 } 1403 1404 override void resize(int left, int top, int width, int height) 1405 { 1406 super.resize(left, top, width, height); 1407 viewport.width = width; 1408 viewport.height = height; 1409 auto child = children.front; 1410 child.resize(0, 0, 1000, 1000); 1411 } 1412 } 1413 1414 extern (C) void signal(int sig, void function(int)); 1415 UiInterface theUi; 1416 extern (C) void windowSizeChangedSignalHandler(int) 1417 { 1418 theUi.resized(); 1419 } 1420 1421 abstract class UiInterface 1422 { 1423 void resized(); 1424 } 1425 1426 class Context 1427 { 1428 Terminal terminal; 1429 int left; 1430 int top; 1431 int width; 1432 int height; 1433 Viewport viewport; 1434 this(Terminal terminal, int left, int top, int width, int height) 1435 { 1436 this.terminal = terminal; 1437 this.left = left; 1438 this.top = top; 1439 this.width = width; 1440 this.height = height; 1441 this.viewport = Viewport(0, 0, width, height); 1442 } 1443 1444 this(Terminal terminal, int left, int top, int width, int height, Viewport viewport) 1445 { 1446 this.terminal = terminal; 1447 this.left = left; 1448 this.top = top; 1449 this.width = width; 1450 this.height = height; 1451 this.viewport = viewport; 1452 } 1453 1454 override string toString() 1455 { 1456 return "Context(left=%s, top=%s, width=%s, height=%s, viewport=%s)".format(left, 1457 top, width, height, viewport); 1458 } 1459 1460 auto forChild(Component c) 1461 { 1462 return new Context(terminal, this.left + c.left, this.top + c.top, c.width, c.height); 1463 } 1464 1465 auto forChild(Component c, Viewport viewport) 1466 { 1467 return new Context(terminal, this.left + c.left, this.top + c.top, 1468 c.width, c.height, viewport); 1469 } 1470 1471 auto putString(int x, int y, string s) 1472 { 1473 int scrolledY = y - viewport.y; 1474 if (scrolledY < 0) 1475 { 1476 return this; 1477 } 1478 if (scrolledY >= viewport.height) 1479 { 1480 return this; 1481 } 1482 // dfmt off 1483 terminal 1484 .xy(left + x, top + scrolledY) 1485 .putString( 1486 s.dropIgnoreAnsiEscapes(viewport.x) 1487 .takeIgnoreAnsiEscapes(viewport.width)); 1488 // dfmt on 1489 return this; 1490 } 1491 } 1492 1493 class Ui : UiInterface 1494 { 1495 Terminal terminal; 1496 Component[] roots; 1497 this(Terminal terminal) 1498 { 1499 this.terminal = terminal; 1500 theUi = this; 1501 signal(SIGWINCH, &windowSizeChangedSignalHandler); 1502 } 1503 1504 auto push(Component root) 1505 { 1506 if (!roots.empty) 1507 { 1508 auto oldRoot = roots[$ - 1]; 1509 oldRoot.lastFocusedComponent = oldRoot.currentFocusedComponent; 1510 oldRoot.clearFocus; 1511 } 1512 roots ~= root; 1513 auto dimension = terminal.dimension; 1514 root.resize(0, 0, dimension.width, dimension.height); 1515 root.focusNext; 1516 return this; 1517 } 1518 1519 auto pop() 1520 { 1521 roots = roots[0 .. $ - 1]; 1522 1523 auto root = roots[$ - 1]; 1524 root.lastFocusedComponent.requestFocus; 1525 root.lastFocusedComponent = null; 1526 return this; 1527 } 1528 1529 void render() 1530 { 1531 try 1532 { 1533 terminal.clearBuffer(); 1534 foreach (root; roots) 1535 { 1536 scope context = new Context(terminal, root.left, root.top, 1537 root.width, root.height); 1538 root.render(context); 1539 } 1540 terminal.flip; 1541 } 1542 catch (Exception e) 1543 { 1544 import std.experimental.logger : error; 1545 1546 e.to!string.error; 1547 } 1548 } 1549 1550 override void resized() 1551 { 1552 auto dimension = terminal.dimension; 1553 foreach (root; roots) 1554 { 1555 root.resize(0, 0, dimension.width, dimension.height); 1556 } 1557 render; 1558 } 1559 1560 void resize() 1561 { 1562 auto dimension = terminal.dimension; 1563 foreach (root; roots) 1564 { 1565 root.resize(0, 0, dimension.width, dimension.height); 1566 } 1567 } 1568 1569 void handleInput(KeyInput input) 1570 { 1571 roots[$ - 1].handleInput(input); 1572 } 1573 } 1574 1575 struct Refresh 1576 { 1577 }