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