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 }