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 }