1 /**
2  * VtScreen module implements VT style terminals (ala XTerm).
3  * These are terminals that work by sending escape sequences over
4  * a single byte stream. Historically this would be a serial port,
5  * but modern systems likely use SSH, or a pty (pseudo-terminal).
6  * Modern Windows has adopted this form of API as well.
7  *
8  * Copyright: Copyright 2025 Garrett D'Amore
9  * Authors: Garrett D'Amore
10  * License:
11  *   Distributed under the Boost Software License, Version 1.0.
12  *   (See accompanying file LICENSE or https://www.boost.org/LICENSE_1_0.txt)
13  *   SPDX-License-Identifier: BSL-1.0
14  */
15 module dcell.vt;
16 
17 package:
18 
19 import core.atomic;
20 import core.time;
21 import std.algorithm : canFind;
22 import std.base64;
23 import std.datetime;
24 import std.exception;
25 import std.format;
26 import std.outbuffer;
27 import std.process;
28 import std.range;
29 import std.stdio;
30 import std.string;
31 
32 import dcell.cell;
33 import dcell.cursor;
34 import dcell.key;
35 import dcell.mouse;
36 import dcell.termio;
37 import dcell.screen;
38 import dcell.event;
39 import dcell.parser;
40 import dcell.tty;
41 
42 class VtScreen : Screen
43 {
44     // Various escape escape sequences we can send.
45     // Note that we have a rather broad assumption that we only support terminals
46     // that understand these things, or in some cases, that will gracefully ignore
47     // them.  (For example, terminals should ignore SGR settings they don't grok.)
48     struct Vt
49     {
50         enum string enableAutoMargin = "\x1b[?7h"; // dec private mode 7 (enable)
51         enum string disableAutoMargin = "\x1b[?7l";
52         enum string setCursorPosition = "\x1b[%d;%dH";
53         enum string sgr0 = "\x1b[m"; // attrOff
54         enum string bold = "\x1b[1m";
55         enum string dim = "\x1b[2m";
56         enum string italic = "\x1b[3m";
57         enum string underline = "\x1b[4m";
58         enum string blink = "\x1b[5m";
59         enum string reverse = "\x1b[7m";
60         enum string strikeThrough = "\x1b[9m";
61         enum string showCursor = "\x1b[?25h";
62         enum string hideCursor = "\x1b[?25l";
63         enum string clear = "\x1b[H\x1b[J";
64         enum string enablePaste = "\x1b[?2004h";
65         enum string disablePaste = "\x1b[?2004l";
66         enum string enableFocus = "\x1b[?1004h";
67         enum string disableFocus = "\x1b[?1004l";
68         enum string cursorReset = "\x1b[0 q"; // reset cursor shape to default
69         enum string cursorBlinkingBlock = "\x1b[1 q";
70         enum string cursorBlock = "\x1b[2 q";
71         enum string cursorBlinkingUnderline = "\x1b[3 q";
72         enum string cursorUnderline = "\x1b[4 q";
73         enum string cursorBlinkingBar = "\x1b[5 q";
74         enum string cursorBar = "\x1b[6 q";
75         enum string enterCA = "\x1b[?1049h"; // alternate screen
76         enum string exitCA = "\x1b[?1049l"; // alternate screen
77         enum string startSyncOut = "\x1b[?2026h";
78         enum string endSyncOut = "\x1b[?2026l";
79         enum string enableAltChars = "\x1b(B\x1b)0"; // set G0 as US-ASCII, G1 as DEC line drawing
80         enum string startAltChars = "\x0e"; // aka Shift-Out
81         enum string endAltChars = "\x0f"; // aka Shift-In
82         enum string enterKeypad = "\x1b[?1h\x1b="; // Note mode 1 might not be supported everywhere
83         enum string exitKeypad = "\x1b[?1l\x1b>"; // Also mode 1
84         enum string setFg8 = "\x1b[3%dm"; // for colors less than 8
85         enum string setFg256 = "\x1b[38;5;%dm"; // for colors less than 256
86         enum string setFgRGB = "\x1b[38;2;%d;%d;%dm"; // for RGB
87         enum string setBg8 = "\x1b[4%dm"; // color colors less than 8
88         enum string setBg256 = "\x1b[48;5;%dm"; // for colors less than 256
89         enum string setBgRGB = "\x1b[48;2;%d;%d;%dm"; // for RGB
90         enum string setFgBgRGB = "\x1b[38;2;%d;%d;%d;48;2;%d;%d;%dm"; // for RGB, in one shot
91         enum string resetFgBg = "\x1b[39;49m"; // ECMA defined
92         enum string requestDA = "\x1b[c"; // request primary device attributes
93         enum string disableMouse = "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l";
94         enum string enableButtons = "\x1b[?1000h";
95         enum string enableDrag = "\x1b[?1002h";
96         enum string enableMotion = "\x1b[?1003h";
97         enum string mouseSgr = "\x1b[?1006h"; // SGR reporting (use with other enables)
98         enum string doubleUnder = "\x1b[4:2m";
99         enum string curlyUnder = "\x1b[4:3m";
100         enum string dottedUnder = "\x1b[4:4m";
101         enum string dashedUnder = "\x1b[4:5m";
102         enum string underColor = "\x1b[58:5:%dm";
103         enum string underRGB = "\x1b[58:2::%d:%d:%dm";
104         enum string underFg = "\x1b[59m";
105 
106         // these can be overridden (e.g. disabled for legacy)
107         string enterURL = "\x1b]8;;%s\x1b\\";
108         string exitURL = "\x1b]8;;\x1b\\";
109         string setWindowSize = "\x1b[8;%d;%dt";
110         // Some terminals do not support the title stack, but do support
111         // changing the title.  For those we set the title back to the
112         // empty string (which they take to mean unset) as a reasonable
113         // fallback. Shell programs generally change this as needed anyway.
114         string saveTitle = "\x1b[22;2t";
115         string restoreTitle = "\x1b]2;\x1b\\" ~ "\x1b[23;2t";
116         string setTitle = "\x1b[>2t\x1b]2;%s\x1b\\";
117         // three advanced keyboard protocols:
118         // - xterm modifyOtherKeys (uses CSI 27 ~ )
119         // - kitty csi-u (uses CSI u)
120         // - win32-input-mode (uses CSI _)
121         string enableCsiU = "\x1b[>4;2m" ~ "\x1b[>1u" ~ "\x1b[?9001h";
122         string disableCsiU = "\x1b[?9001l" ~ "\x1b[<u" ~ "\x1b[>4;0m";
123 
124         // OSC 52 is for saving to the clipboard.
125         // This string takes a base64 string and sends it to the clipboard.
126         // It will also be able to retrieve the clipboard using "?" as the
127         // sent string, when we support that.
128         string setClipboard = "\x1b]52;c;%s\x1b\\";
129 
130         // number of colors - again this can be overridden.
131         // Typical values are 0 (monochrome), 8, 16, 256, and 1<<24.
132         // There are some oddballs like xterm-88color.  The first
133         // 256 colors are from the xterm palette, but if something
134         // supports more, it is assumed to support direct RGB colors.
135         // Pretty much most modern terminals support 256, which is why
136         // we use it as a default.  (This can be affected by environment
137         // variables.)
138         int numColors = 256;
139 
140         // requestWindowSize = "\x1b[18t"                          // For modern terminals
141     }
142 
143     this()
144     {
145         version (Posix)
146         {
147             import dcell.termio : PosixTty;
148 
149             this(new PosixTty("/dev/tty"), "");
150         }
151         else version (Windows)
152         {
153             import dcell.wintty : WinTty;
154 
155             this(new WinTty());
156         }
157         else
158         {
159             throw new Exception("no default TTY for platform");
160         }
161     }
162 
163     this(Tty tt, string term = "")
164     {
165         ti = tt;
166         ti.start();
167         cells = new CellBuffer(ti.windowSize());
168         evq = new TtyEventQ();
169         ob = new OutBuffer();
170         cells.style.bg = Color.reset;
171         cells.style.fg = Color.reset;
172 
173         if (term == "")
174         {
175             term = environment.get("TERM");
176         }
177 
178         legacy = false;
179         if (term.startsWith("vt") || term.canFind("ansi") || term == "linux" || term == "sun" || term == "sun-color")
180         {
181             // these terminals are "legacy" and not expected to support most OSC functions
182             legacy = true;
183         }
184 
185         string cterm = environment.get("COLORTERM");
186         if ("NO_COLOR" in environment)
187         {
188             vt.numColors = 0;
189         }
190         else if (cterm == "truecolor" || cterm == "24bit" || cterm == "24-bit")
191         {
192             vt.numColors = 1 << 24;
193         }
194         else if (term.endsWith("256color") || cterm.canFind("256"))
195         {
196             vt.numColors = 256;
197         }
198         else if (term.endsWith("88color"))
199         {
200             vt.numColors = 88;
201         }
202         else if (term.endsWith("16color"))
203         {
204             vt.numColors = 16;
205         }
206         else if (cterm != "")
207         {
208             vt.numColors = 8;
209         }
210         else if (term.endsWith("-m") || term.canFind("mono") || term.startsWith("vt"))
211         {
212             vt.numColors = 0;
213         }
214         else if (term.endsWith("color") || term.canFind("ansi"))
215         {
216             vt.numColors = 8;
217         }
218         else if (term == "dtterm" || term == "aixterm" || term == "linux")
219         {
220             vt.numColors = 8;
221         }
222         else if (term == "sun")
223         {
224             vt.numColors = 0;
225         }
226 
227         if (environment.get("DCELL_ALTSCREEN") == "disable")
228         {
229             altScrEn = false;
230         }
231         else
232         {
233             altScrEn = true;
234         }
235 
236         version (Windows)
237         {
238             // If we don't have a $TERM (e.g. Windows Terminal), or we are dealing with WezTerm
239             // (which cannot mix modes), then only support win32-input-mode.
240             if (term == "")
241             {
242                 vt.enableCsiU = "\x1b[?9001h";
243                 vt.disableCsiU = "\x1b[?9001l";
244             }
245         }
246 
247         if (legacy)
248         {
249             vt.enterURL = null;
250             vt.exitURL = null;
251             vt.setWindowSize = null;
252             vt.setTitle = null;
253             vt.restoreTitle = null;
254             vt.saveTitle = null;
255             vt.enableCsiU = null;
256             vt.disableCsiU = null;
257             vt.setClipboard = null;
258         }
259     }
260 
261     ~this()
262     {
263         ti.close();
264     }
265 
266     void start()
267     {
268         if (started)
269             return;
270 
271         parser = new Parser(); // if we are restarting, this discards the old one
272         ti.save();
273         ti.raw();
274         if (altScrEn)
275         {
276             puts(vt.enterCA);
277         }
278         puts(vt.hideCursor);
279         puts(vt.disableAutoMargin);
280         puts(vt.enableCsiU);
281         puts(vt.saveTitle);
282         puts(vt.enterKeypad);
283         puts(vt.enableFocus);
284         puts(vt.enableAltChars);
285         puts(vt.clear);
286         if (title && !vt.setTitle.empty)
287         {
288             puts(format(vt.setTitle, title));
289         }
290 
291         resize();
292         draw();
293 
294         started = true;
295     }
296 
297     void stop()
298     {
299         if (!started)
300             return;
301 
302         puts(vt.enableAutoMargin);
303         puts(vt.resetFgBg);
304         puts(vt.sgr0);
305         puts(vt.cursorReset);
306         puts(vt.showCursor);
307         puts(vt.cursorReset);
308         puts(vt.restoreTitle);
309         if (altScrEn)
310         {
311             puts(vt.clear);
312             puts(vt.exitCA);
313         }
314         puts(vt.exitKeypad);
315         puts(vt.disablePaste);
316         puts(vt.disableMouse);
317         puts(vt.disableFocus);
318         puts(vt.disableCsiU);
319         flush();
320         ti.stop();
321         ti.restore();
322         started = false;
323     }
324 
325     void clear() @safe
326     {
327         // save the style currently in effect, so when
328         // we later send the clear, we can use it.
329         baseStyle = style;
330         fill(" ");
331         clear_ = true;
332         // because we are going to clear it in the next cycle,
333         // lets mark all the cells clean, so that we don't waste
334         // needless time redrawing spaces for the entire screen.
335         cells.setAllDirty(false);
336     }
337 
338     void fill(string s, Style style) @safe
339     {
340         cells.fill(s, style);
341     }
342 
343     void fill(string s) @safe
344     {
345         fill(s, this.style);
346     }
347 
348     void showCursor(Coord pos, Cursor cur = Cursor.current)
349     {
350         // just save the coordinates for now
351         // it will be used during the next draw cycle
352         cursorPos = pos;
353         cursorShape = cur;
354     }
355 
356     void showCursor(Cursor cur)
357     {
358         cursorShape = cur;
359     }
360 
361     Coord size() pure const
362     {
363         return (cells.size());
364     }
365 
366     void resize() @safe
367     {
368         auto phys = ti.windowSize();
369         if (phys != cells.size())
370         {
371             cells.resize(phys);
372             cells.setAllDirty(true);
373         }
374     }
375 
376     ref Cell opIndex(size_t x, size_t y) @safe
377     {
378         return (cells[x, y]);
379     }
380 
381     void opIndexAssign(Cell c, size_t x, size_t y) @safe
382     {
383         cells[x, y] = c;
384     }
385 
386     void enablePaste(bool b) @safe
387     {
388         pasteEn = b;
389         sendPasteEnable(b);
390     }
391 
392     int colors() const pure nothrow @safe
393     {
394         return vt.numColors;
395     }
396 
397     void show() @safe
398     {
399         resize();
400         draw();
401     }
402 
403     void sync() @safe
404     {
405         pos_ = Coord(-1, -1);
406         resize();
407         clear_ = true;
408         cells.setAllDirty(true);
409         draw();
410     }
411 
412     void beep() @safe
413     {
414         puts("\x07");
415         flush();
416     }
417 
418     void setSize(Coord size) @safe
419     {
420         if (vt.setWindowSize != "")
421         {
422             puts(format(vt.setWindowSize, size.y, size.x));
423             flush();
424             cells.setAllDirty(true);
425             resize();
426         }
427     }
428 
429     void enableMouse(MouseEnable en) @safe
430     {
431         // we rely on the fact that all known implementations adhere
432         // to the de-facto standard from XTerm.  This is necessary as
433         // there is no standard terminfo sequence for reporting this
434         // information.
435         mouseEn = en; // save this so we can restore after a suspend
436         sendMouseEnable(en);
437     }
438 
439     void enableAlternateScreen(bool enabled) @safe
440     {
441         altScrEn = enabled;
442         if (environment.get("DCELL_ALTSCREEN") == "disable")
443         {
444             altScrEn = false;
445         }
446     }
447 
448     void setTitle(string title) @safe
449     {
450         this.title = title;
451         if (started && !vt.setTitle.empty)
452         {
453             puts(format(vt.setTitle, title));
454             flush();
455         }
456     }
457 
458     bool waitForEvent(Duration timeout, ref Duration resched) @safe
459     {
460         // expire for a time when we will timeout, safeguard against obvious overflow.
461         MonoTime expire = (timeout == Duration.max) ? MonoTime.max : MonoTime.currTime() + timeout;
462 
463         // residual tracks whether we are waiting for the rest of
464         // a partial escape sequence in the parser.
465         bool residual = false;
466         bool readOnce = false;
467 
468         for (;;)
469         {
470             evq ~= parser.events();
471             if (ti.resized())
472             {
473                 Event rev;
474                 rev.type = EventType.resize;
475                 rev.when = MonoTime.currTime();
476                 evq ~= rev;
477             }
478             if (!evq.empty)
479             {
480                 return true;
481             }
482 
483             MonoTime now = MonoTime.currTime();
484 
485             // if we expired, and we haven't at least called the
486             // read function once, then return.
487             Duration interval = expire - now;
488 
489             if (expire < now)
490             {
491                 if (readOnce)
492                 {
493                     resched = residual ? msecs(25) : Duration.max;
494                     return false;
495                 }
496                 interval = msecs(0); // just do a polling read
497             }
498 
499             readOnce = true;
500 
501             // if we have partial data in the parser, we need to use
502             // a shorter wakeup, so we can create an event in case the
503             // escape sequence is not completed (e.g. lone ESC.)
504             if (residual && interval > msecs(5))
505             {
506                 interval = msecs(5);
507             }
508 
509             residual = !parser.parse(ti.read(interval));
510         }
511     }
512 
513     EventQ events() nothrow @safe @nogc
514     {
515         return evq;
516     }
517 
518     // This is the default style we use when writing content using
519     // put and similar APIs.
520     @property ref Style style() @safe
521     {
522         return cells.style;
523     }
524 
525     @property Style style(const(Style) st) @safe
526     {
527         return cells.style = st;
528     }
529 
530     // This is the current position that will be writing when when using
531     // put or write.
532     @property Coord position() const @safe
533     {
534         return cells.position;
535     }
536 
537     @property Coord position(const(Coord) pos) @safe
538     {
539         return cells.position = pos;
540     }
541 
542     // Write a string at the current `position`, using the current `style`.
543     // This will wrap if it reaches the end of the terminal.
544     void write(string s) @safe
545     {
546         cells.write(s);
547     }
548 
549     void write(wstring s) @safe
550     {
551         cells.write(s);
552     }
553 
554     void write(dstring s) @safe
555     {
556         cells.write(s);
557     }
558 
559     void setClipboard(const(ubyte[]) b) @safe
560     {
561         if (!vt.setClipboard.empty)
562         {
563             puts(format(vt.setClipboard, Base64.encode(b)));
564             flush();
565         }
566     }
567 
568     void getClipboard() @safe
569     {
570         if (!vt.setClipboard.empty)
571         {
572             puts(format(vt.setClipboard, "?"));
573             flush();
574         }
575     }
576 
577 private:
578     struct KeyCode
579     {
580         Key key;
581         Modifiers mod;
582     }
583 
584     class TtyEventQ : EventQ
585     {
586         override void put(Event ev) @safe
587         {
588             super.put(ev);
589             ti.wakeUp();
590         }
591 
592         // Note that this operator (~=) intentionally
593         // calls the parents put directly to prevent spurious wakeups
594         // when adding events that have already come from the tty.
595         // It is significant that this method (indeed the entire class)
596         // is private, so it should not be accessible by external consumers.
597         void opOpAssign(string op : "~")(Event rhs) nothrow @safe
598         {
599             super.put(rhs);
600         }
601 
602         // Permit appending a list of events read from the parser directly, but
603         // without waking up the reader.
604         void opOpAssign(string op : "~")(Event[] rhs) nothrow @safe
605         {
606             foreach (ev; rhs)
607             {
608                 super.put(ev);
609             }
610         }
611     }
612 
613     CellBuffer cells;
614     bool clear_; // if a screen clear is requested
615     Coord pos_; // location where we will update next
616     Style style_; // current style
617     Style baseStyle;
618     Coord cursorPos;
619     Cursor cursorShape;
620     MouseEnable mouseEn; // saved state for suspend/resume
621     bool pasteEn; // saved state for suspend/resume
622     bool altScrEn; // alternate screen is enabled (default on)
623     Tty ti;
624     OutBuffer ob;
625     bool started;
626     bool legacy; // legacy terminals don't have support for OSC, APC, DSC, etc.
627     Vt vt;
628     Parser parser;
629     string title;
630     TtyEventQ evq;
631 
632     void puts(string s) @safe
633     {
634         ob.write(s);
635     }
636 
637     // flush queued output
638     void flush() @safe
639     {
640         ti.write(ob.toString());
641         ti.flush();
642         ob.clear();
643     }
644 
645     // sendColors sends just the colors for a given style
646     void sendColors(Style style) @safe
647     {
648         auto fg = style.fg;
649         auto bg = style.bg;
650 
651         if (vt.numColors == 0 || (fg == Color.invalid && bg == Color.invalid))
652         {
653             return;
654         }
655 
656         if (style.ul.isValid && (style.attr & Attr.underlineMask))
657         {
658             if (style.ul == Color.reset)
659             {
660                 puts(vt.underFg);
661             }
662             else if (style.ul.isRGB && vt.numColors > 256)
663             {
664                 auto rgb = decompose(style.ul);
665                 puts(format!(vt.underRGB)(rgb[0], rgb[1], rgb[2]));
666             }
667             else
668             {
669                 auto ul = toPalette(style.ul, vt.numColors);
670                 puts(format!(vt.underColor)(ul));
671             }
672         }
673         if (fg == Color.reset || bg == Color.reset)
674         {
675             puts(vt.resetFgBg);
676         }
677         if (vt.numColors > 256)
678         {
679             if (isRGB(fg) && isRGB(bg))
680             {
681                 auto rgb1 = decompose(fg);
682                 auto rgb2 = decompose(bg);
683                 puts(format!(vt.setFgBgRGB)(rgb1[0], rgb1[1], rgb1[2], rgb2[0], rgb2[1], rgb2[2]));
684                 return;
685             }
686             if (isRGB(fg))
687             {
688                 auto rgb = decompose(fg);
689                 puts(format!(vt.setFgRGB)(rgb[0], rgb[1], rgb[2]));
690                 fg = Color.invalid;
691             }
692             if (isRGB(bg))
693             {
694                 auto rgb = decompose(bg);
695                 puts(format!(vt.setBgRGB)(rgb[0], rgb[1], rgb[2]));
696                 bg = Color.invalid;
697             }
698         }
699 
700         fg = toPalette(fg, vt.numColors);
701         bg = toPalette(bg, vt.numColors);
702 
703         if (fg < 8)
704         {
705             puts(format!(vt.setFg8)(fg));
706         }
707         else if (fg < 256)
708         {
709             puts(format!(vt.setFg256)(fg));
710         }
711         if (bg < 8)
712         {
713             puts(format!(vt.setBg8)(bg));
714         }
715         else if (bg < 256)
716         {
717             puts(format!(vt.setBg256)(bg));
718         }
719     }
720 
721     void sendAttrs(Style style) @safe
722     {
723         auto attr = style.attr;
724         if (attr & Attr.bold)
725             puts(vt.bold);
726         if (attr & Attr.reverse)
727             puts(vt.reverse);
728         if (attr & Attr.blink)
729             puts(vt.blink);
730         if (attr & Attr.dim)
731             puts(vt.dim);
732         if (attr & Attr.italic)
733             puts(vt.italic);
734         if (attr & Attr.strikethrough)
735             puts(vt.strikeThrough);
736         switch (attr & Attr.underlineMask)
737         {
738         case Attr.plainUnderline:
739             puts(vt.underline);
740             break;
741         case Attr.doubleUnderline:
742             puts(vt.underline);
743             puts(vt.doubleUnder);
744             break;
745         case Attr.curlyUnderline:
746             puts(vt.underline);
747             puts(vt.curlyUnder);
748             break;
749         case Attr.dottedUnderline:
750             puts(vt.underline);
751             puts(vt.dottedUnder);
752             break;
753         case Attr.dashedUnderline:
754             puts(vt.underline);
755             puts(vt.dashedUnder);
756             break;
757         default:
758             break;
759         }
760     }
761 
762     void clearScreen() @safe
763     {
764         if (clear_)
765         {
766             // We want to use the style that was in effect
767             // when the clear function was called.
768             Style savedStyle = style;
769             style = baseStyle;
770             clear_ = false;
771             puts(vt.sgr0);
772             puts(vt.exitURL);
773             sendColors(style);
774             sendAttrs(style);
775             style_ = style;
776             puts(Vt.clear);
777             flush();
778             style = savedStyle;
779         }
780     }
781 
782     void goTo(Coord pos) @safe
783     {
784         if (pos != pos_)
785         {
786             puts(format!(vt.setCursorPosition)(pos.y + 1, pos.x + 1));
787             pos_ = pos;
788         }
789     }
790 
791     // sendCursor sends the current cursor location
792     void sendCursor() @safe
793     {
794         if (!cells.isLegal(cursorPos) || (cursorShape == Cursor.hidden))
795         {
796             puts(vt.hideCursor);
797             return;
798         }
799         goTo(cursorPos);
800         puts(cursorShape != Cursor.hidden ? vt.showCursor : vt.hideCursor);
801         final switch (cursorShape)
802         {
803         case Cursor.current:
804             break;
805         case Cursor.hidden:
806             break;
807         case Cursor.reset:
808             puts(vt.cursorReset);
809             break;
810         case Cursor.bar:
811             puts(vt.cursorBar);
812             break;
813         case Cursor.block:
814             puts(vt.cursorBlock);
815             break;
816         case Cursor.underline:
817             puts(vt.cursorUnderline);
818             break;
819         case Cursor.blinkingBar:
820             puts(vt.cursorBlinkingBar);
821             break;
822         case Cursor.blinkingBlock:
823             puts(vt.cursorBlinkingBlock);
824             break;
825         case Cursor.blinkingUnderline:
826             puts(vt.cursorBlinkingUnderline);
827             break;
828         }
829 
830         // update our location
831         pos_ = cursorPos;
832     }
833 
834     // drawCell draws one cell.  It returns the width drawn (1 or 2).
835     int drawCell(Coord pos) @safe
836     {
837         Cell c = cells[pos];
838         auto insert = false;
839         if (!cells.dirty(pos))
840         {
841             return c.width;
842         }
843         auto size = cells.size();
844         if (pos != pos_)
845         {
846             goTo(pos);
847         }
848 
849         if (vt.numColors == 0)
850         {
851             // if its monochrome, simulate lighter and darker with reverse
852             if (darker(c.style.fg, c.style.bg))
853             {
854                 c.style.attr ^= Attr.reverse;
855             }
856         }
857         if (vt.enterURL == "")
858         {
859             // avoid pointless changes due to URL where not supported
860             c.style.url = "";
861         }
862 
863         if (c.style.fg != style_.fg || c.style.bg != style_.bg || c.style.attr != style_.attr)
864         {
865             puts(Vt.sgr0);
866             sendColors(c.style);
867             sendAttrs(c.style);
868         }
869         if (c.style.url != style_.url)
870         {
871             if (c.style.url != "" && vt.enterURL !is null)
872             {
873                 puts(format(vt.enterURL, c.style.url));
874             }
875             else
876             {
877                 puts(vt.exitURL);
878             }
879         }
880         // TODO: replacement encoding (ACSC, application supplied fallbacks)
881 
882         style_ = c.style;
883 
884         if (pos.x + c.width > size.x)
885         {
886             // if too big to fit last column, just fill with a space
887             c.text = " ";
888         }
889 
890         puts(c.text);
891         pos_.x += c.width;
892         // Note that we might be beyond the width, and if auto-margin
893         // is set true, we might have wrapped.  But it turns out that
894         // we can't reliably depend on auto-margin, as some terminals
895         // that claim to behave that way actually don't.
896         cells.setDirty(pos, false);
897         if (insert)
898         {
899             // go back and redraw the second to last cell
900             drawCell(Coord(pos.x - 1, pos.y));
901         }
902         return c.width;
903     }
904 
905     void draw() @safe
906     {
907         puts(vt.startSyncOut);
908         puts(vt.hideCursor); // hide the cursor while we draw
909         clearScreen(); // no op if not needed
910         auto size = cells.size();
911         Coord pos = Coord(0, 0);
912         for (pos.y = 0; pos.y < size.y; pos.y++)
913         {
914             int width = 1;
915             for (pos.x = 0; pos.x < size.x; pos.x += width)
916             {
917                 width = drawCell(pos);
918                 // this way if we ever redraw that cell, it will
919                 // be marked dirty, because we have clobbered it with
920                 // the adjacent character
921                 if (width < 1)
922                     width = 1;
923                 if (width > 1)
924                 {
925                     cells.setDirty(Coord(pos.x + 1, pos.y), true);
926                 }
927             }
928         }
929         sendCursor();
930         puts(vt.endSyncOut);
931         flush();
932     }
933 
934     void sendMouseEnable(MouseEnable en) @safe
935     {
936         // we rely on the fact that all known implementations adhere
937         // to the de-facto standard from XTerm.  This is necessary as
938         // there is no standard terminfo sequence for reporting this
939         // information.
940         // start by disabling everything
941         puts(vt.disableMouse);
942         // then turn on specific enables
943         if (en & MouseEnable.buttons)
944         {
945             puts(vt.enableButtons);
946         }
947         if (en & MouseEnable.drag)
948         {
949             puts(vt.enableDrag);
950         }
951         if (en & MouseEnable.motion)
952         {
953             puts(vt.enableMotion);
954         }
955         // and if any are set, we need to send this
956         if (en & MouseEnable.all)
957         {
958             puts(vt.mouseSgr);
959         }
960         flush();
961     }
962 
963     void sendPasteEnable(bool b) @safe
964     {
965         puts(b ? Vt.enablePaste : Vt.disablePaste);
966         flush();
967     }
968 }