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