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 }