1 module aurorafw.cli.terminal.terminal; 2 3 import aurorafw.core.input.events; 4 import aurorafw.core.input.keys; 5 6 version (Posix) 7 { 8 import core.sys.posix.unistd; 9 import core.sys.posix.termios; 10 import core.sys.posix.sys.ioctl; 11 } 12 else version (Windows) 13 { 14 import core.sys.windows.windows; 15 } 16 17 import std.process; 18 import std..string; 19 import std.exception; 20 import std.utf; 21 import std.typecons; 22 import std.file; 23 import std.stdio; 24 import std.ascii : isAlpha; 25 import std.uni; 26 import std.conv; 27 28 import core.stdc.errno; 29 import core.stdc.stdio; 30 31 import riverd.ncurses; 32 33 /** Exception for terminal die 34 * This exception is thrown when theres a problem with 35 * terminal related issue. 36 */ 37 class TerminalDieException : Exception 38 { 39 /** Terminal Die Exception Constructor 40 * This construct a normal exception and disable 41 * terminal raw mode. 42 * @param term Terminal 43 * @param msg exception message 44 * @param file code file 45 * @param line line code on file 46 * @param next next throwable object 47 */ 48 this(ref Terminal term, string msg, 49 string file = __FILE__, size_t line = __LINE__, 50 Throwable next = null) 51 { 52 term.terminate(); 53 super(msg, file, line, next); 54 } 55 56 /** Terminal Die Exception Constructor 57 * This construct a normal exception and disable 58 * terminal raw mode. 59 * @param term Terminal 60 * @param msg exception message 61 * @param next next throwable object 62 * @param file code file 63 * @param line line code on file 64 */ 65 this(ref Terminal term, string msg, Throwable next, 66 string file = __FILE__, size_t line = __LINE__) 67 { 68 this(term, msg, file, line, next); 69 } 70 } 71 72 // TODO: Switch special writes to termcap codes 73 74 /** Terminal Struct 75 * This struct represents a terminal buffer and 76 * a set of functions to manipulate it under the 77 * defined i/o descriptors. 78 */ 79 struct Terminal 80 { 81 // disable empty and copy constructors 82 @disable this(); 83 @disable this(this); 84 85 /** Terminal Output Type 86 * Enumerates the terminal output type 87 */ 88 public enum OutputType 89 { 90 CELL, 91 MINIMAL, 92 LINEAR 93 } 94 95 /** Constructor for Terminal 96 * This construct a terminal buffer with the specified 97 * output type and i/o descriptors 98 * @param outType Output Type 99 * @param outputDescriptor Output descriptor 100 * @param inputDescriptor Input descriptor 101 */ 102 public this(OutputType outType, int outputDescriptor = STDOUT_FILENO, int inputDescriptor = STDIN_FILENO) 103 { 104 this.outType = outType; 105 this.outputDescriptor = outputDescriptor; 106 this.inputDescriptor = inputDescriptor; 107 108 initscr(); 109 110 // for cell based output (grid-style) 111 if (outType == OutputType.CELL) 112 { 113 saveTitle(); 114 enableRawMode(); 115 } 116 } 117 118 /** Destructor for Terminal 119 * 120 * This destruct a terminal buffer after terminating it. 121 */ 122 ~this() 123 { 124 terminate(); 125 } 126 127 /** Terminate terminal mode 128 * 129 * This function terminate all terminal modes and restore to the normal 130 * mode. 131 */ 132 package void terminate() 133 { 134 if (outType == OutputType.CELL) 135 { 136 disableRawMode(true); 137 restoreTitle(true); 138 } 139 140 endwin(); 141 } 142 143 public void enableRawMode() 144 { 145 if (rawMode == true) 146 return; 147 rawMode = true; 148 149 if (tcgetattr(inputDescriptor, &origTermios) == -1) 150 throw new TerminalDieException(this, "tcgetattr"); 151 152 termios raw = origTermios; 153 154 raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); 155 raw.c_oflag &= ~(OPOST); 156 raw.c_cflag |= (CS8); 157 raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); 158 raw.c_cc[VMIN] = 0; 159 raw.c_cc[VTIME] = 1; 160 161 cbreak(); 162 163 if (tcsetattr(inputDescriptor, TCSAFLUSH, &raw) == -1) 164 throw new TerminalDieException(this, "tcsetattr"); 165 166 alternateScreen(true); 167 168 keypad(stdscr, true); 169 nodelay(stdscr, true); 170 171 noecho(); 172 } 173 174 public void viewCursor(bool val, bool flush = true) 175 { 176 string ret; 177 if (val) 178 ret = "\x1b[?25h"; 179 else 180 ret = "\x1b[?25l"; 181 182 if (flush) 183 writeDescriptor(ret); 184 else 185 writeBuffer(ret); 186 } 187 188 public void alternateScreen(bool val, bool flush = false) 189 { 190 string ret; 191 if (val) 192 ret = "\x1b[?1049h"; 193 else 194 ret = "\x1b[?1049l"; 195 196 if (flush) 197 writeDescriptor(ret); 198 else 199 writeBuffer(ret); 200 } 201 202 public void disableRawMode(bool flush = false) 203 { 204 if (rawMode == false) 205 return; 206 rawMode = false; 207 208 alternateScreen(false, flush); 209 210 if (tcsetattr(inputDescriptor, TCSAFLUSH, &origTermios) == -1) 211 throw new TerminalDieException(this, "tcsetattr"); 212 } 213 214 public void clear(bool flush = false) 215 { 216 if (outType == OutputType.CELL) 217 { 218 string ret = "\x1b[2J\x1b[H"; 219 if (flush) 220 writeDescriptor(ret); 221 else 222 writeBuffer(ret); 223 } 224 } 225 226 @safe 227 public void setCursorPos(int x = 0, int y = 0, bool flush = false) 228 { 229 string ret = "\x1b[" ~ to!string(y + 1) ~ ";" ~ to!string(x + 1) ~ "H"; 230 if (flush) 231 writeDescriptor(ret); 232 else 233 writeBuffer(ret); 234 } 235 236 @safe 237 public void moveXPos(int val = 0, bool flush = false) 238 { 239 string ret = "\x1b[" ~ to!string(val); 240 241 // check if forward (+) or backward (-) 242 if (val > 0) 243 ret ~= "C"; 244 else 245 ret ~= "D"; 246 247 if (flush) 248 writeDescriptor(ret); 249 else 250 writeBuffer(ret); 251 } 252 253 @safe 254 public void moveYPos(int val = 0, bool flush = false) 255 { 256 string ret = "\x1b[" ~ to!string(val); 257 258 // check if forward (+) or backward (-) 259 if (val > 0) 260 ret ~= 'B'; 261 else 262 ret ~= 'A'; 263 264 if (flush) 265 writeDescriptor(ret); 266 else 267 writeBuffer(ret); 268 } 269 270 @safe 271 public void writeBuffer(char[] buf) pure 272 { 273 buffer ~= buf; 274 } 275 276 @safe 277 public void writeBuffer(dchar ch) pure 278 { 279 buffer ~= ch; 280 } 281 282 @safe 283 public void writeBuffer(string str) pure 284 { 285 buffer ~= str; 286 } 287 288 @safe 289 public void writeDescriptor(string str) 290 { 291 writeDescriptor(str.toStringz, str.length); 292 } 293 294 public void flushBuffer() 295 { 296 refresh(); 297 if (buffer.length > 0) 298 { 299 writeDescriptor(buffer.toStringz, buffer.length); 300 buffer.length = 0; 301 } 302 } 303 304 @trusted 305 public void writeDescriptor(const(char*) cstr, size_t len) 306 { 307 //TODO: Check for error handling 308 core.sys.posix.unistd.write(outputDescriptor, cstr, len); 309 } 310 311 public size_t readCh(ref dchar c) 312 { 313 size_t nread; 314 char[1] buf; 315 316 nread = .read(inputDescriptor, &buf[0], buf.length); 317 if (nread == -1 && errno != EAGAIN) 318 throw new TerminalDieException(this, "read"); 319 if (nread == 0) 320 c = 0; 321 else 322 { 323 char[] dbuf; 324 foreach (ch; buf[0 .. nread]) 325 dbuf ~= ch; 326 if (dbuf.length && dbuf.length >= dbuf.stride()) 327 c = dbuf.decodeFront!(Yes.useReplacementDchar); 328 else 329 c = 0; 330 } 331 332 return nread; 333 } 334 335 public string readBuffer() 336 { 337 size_t nread; 338 string buf; 339 340 do 341 { 342 dchar c; 343 nread = readCh(c); 344 if (nread != 0) 345 buf ~= c; 346 } 347 while (nread != 0); 348 349 return buf; 350 } 351 352 public int getWindowSize(ref int rows, ref int cols) 353 { 354 winsize ws; 355 356 if (ioctl(outputDescriptor, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) 357 { 358 if (core.sys.posix.unistd.write(outputDescriptor, "\x1b[999C\x1b[999B".toStringz, 12) != 12) 359 return -1; 360 return getCursorPosition(rows, cols); 361 } 362 else 363 { 364 rows = ws.ws_row; 365 cols = ws.ws_col; 366 return 0; 367 } 368 } 369 370 public int getCursorPosition(ref int rows, ref int cols) 371 { 372 char[32] buf; 373 uint i = 0; 374 375 if (core.sys.posix.unistd.write(outputDescriptor, "\x1b[6n".toStringz, 4) != 4) 376 return -1; 377 378 while (i < buf.sizeof - 1) 379 { 380 if (.read(inputDescriptor, &buf[i], 1) != 1) 381 break; 382 if (buf[i] == 'R') 383 break; 384 i++; 385 } 386 387 buf[i] = '\0'; 388 389 if (buf[0] != '\x1b' || buf[1] != '[') 390 return -1; 391 if (sscanf(&buf[2], "%d;%d", &rows, &cols) != 2) 392 return -1; 393 394 return 0; 395 } 396 397 @property public bool isRawMode() 398 { 399 return rawMode; 400 } 401 402 public void setTitle(string title) 403 { 404 version (Windows) 405 { 406 SetConsoleTitleA(toStringz(title)); 407 } 408 else 409 { 410 if (terminalInFamily("xterm", "rxvt", "screen")) 411 writeBuffer(format("\x1b]0;%s\007", title)); 412 } 413 } 414 415 private void saveTitle(bool flush = false) 416 { 417 version (Posix) 418 { 419 if (terminalInFamily("xterm", "rxvt", "screen")) 420 { 421 if (flush) 422 writeDescriptor("\x1b[22;0t"); 423 else 424 writeBuffer("\x1b[22;0t"); 425 } 426 } 427 } 428 429 private void restoreTitle(bool flush = false) 430 { 431 version (Posix) 432 { 433 if (terminalInFamily("xterm", "rxvt", "screen")) 434 { 435 if (flush) 436 writeDescriptor("\x1b[23;0t"); 437 else 438 writeBuffer("\x1b[23;0t"); 439 } 440 } 441 } 442 443 public Event pollEvents() 444 { 445 Event ret; 446 if (stdscr is null) 447 throw new TerminalDieException(this, "null stdscr"); 448 449 int c = getch(); 450 while (c != -1) 451 { 452 ret.flags |= processEvent(c); 453 ++ret.eventsProcessed; 454 455 c = getch(); 456 } 457 458 if (input.buf.length > 0) 459 ret.flags |= EventFlag.InputBuffer; 460 461 if (buffer.length > 0) 462 ret.flags |= EventFlag.RawBuffer; 463 464 return ret; 465 } 466 467 public bool keyHit() 468 { 469 int ch = getch(); 470 471 if (ch != ERR) 472 { 473 ungetch(ch); 474 return true; 475 } 476 else 477 { 478 return false; 479 } 480 } 481 482 private EventFlag processEvent(int ev) 483 { 484 KeyboardEvent kret; 485 MouseScrollEvent sret; 486 EventFlag flags; 487 488 switch (ev) 489 { 490 // dfmt off 491 case KEY_BACKSPACE: 492 case 127: 493 case '\b': 494 kret.key = Keycode.Backspace; break; 495 case KEY_SEND: kret.mods = InputModifier.Shift; goto case; 496 case KEY_END: kret.key = Keycode.End; break; 497 case KEY_SHOME: kret.mods = InputModifier.Shift; goto case; 498 case KEY_HOME: kret.key = Keycode.Home; break; 499 case KEY_SF: kret.mods = InputModifier.Shift; goto case; 500 case KEY_DOWN: kret.key = Keycode.Down; break; 501 case KEY_SR: kret.mods = InputModifier.Shift; goto case; 502 case KEY_UP: kret.key = Keycode.Up; break; 503 case KEY_SLEFT: kret.mods = InputModifier.Shift; goto case; 504 case KEY_LEFT: kret.key = Keycode.Left; break; 505 case KEY_SRIGHT: kret.mods = InputModifier.Shift; goto case; 506 case KEY_RIGHT: kret.key = Keycode.Right; break; 507 case KEY_F(1): kret.key = Keycode.F1; break; 508 case KEY_F(2): kret.key = Keycode.F2; break; 509 case KEY_F(3): kret.key = Keycode.F3; break; 510 case KEY_F(4): kret.key = Keycode.F4; break; 511 case KEY_F(5): kret.key = Keycode.F5; break; 512 case KEY_F(6): kret.key = Keycode.F6; break; 513 case KEY_F(7): kret.key = Keycode.F7; break; 514 case KEY_F(8): kret.key = Keycode.F8; break; 515 case KEY_F(9): kret.key = Keycode.F9; break; 516 case KEY_F(10): kret.key = Keycode.F10; break; 517 case KEY_F(11): kret.key = Keycode.F11; break; 518 case KEY_F(12): kret.key = Keycode.F12; break; 519 case KEY_F(13): kret.key = Keycode.F13; break; 520 case KEY_F(14): kret.key = Keycode.F14; break; 521 case KEY_F(15): kret.key = Keycode.F15; break; 522 case KEY_F(16): kret.key = Keycode.F16; break; 523 case KEY_F(17): kret.key = Keycode.F17; break; 524 case KEY_F(18): kret.key = Keycode.F18; break; 525 case KEY_F(19): kret.key = Keycode.F19; break; 526 case KEY_F(20): kret.key = Keycode.F20; break; 527 case KEY_F(21): kret.key = Keycode.F21; break; 528 case KEY_F(22): kret.key = Keycode.F22; break; 529 case KEY_F(23): kret.key = Keycode.F23; break; 530 case KEY_F(24): kret.key = Keycode.F24; break; 531 case KEY_F(25): kret.key = Keycode.F25; break; 532 case KEY_ENTER: kret.key = Keycode.Enter; break; 533 case KEY_PPAGE: kret.key = Keycode.PageUp; break; 534 case KEY_NPAGE: kret.key = Keycode.PageDown; break; 535 case KEY_SIC: kret.mods = InputModifier.Shift; goto case; 536 case KEY_IC: kret.key = Keycode.Insert; break; 537 case KEY_SDC: kret.mods = InputModifier.Shift; goto case; 538 case KEY_DC: kret.key = Keycode.Delete; break; 539 case KEY_RESIZE: flags |= EventFlag.Resize; break; 540 case 560: // CTRL + Up 541 kret.key = Keycode.Up; 542 kret.mods = InputModifier.Control; 543 break; 544 case 519: // CTRL + Down 545 kret.key = Keycode.Down; 546 kret.mods = InputModifier.Control; 547 break; 548 549 default: 550 kret = processSpecialKeyEvent(ev); 551 // dfmt on 552 } 553 554 if (input.keyPressedCallback !is null) 555 input.keyPressedCallback(kret); 556 557 return flags; 558 } 559 560 private KeyboardEvent processSpecialKeyEvent(int ev) 561 { 562 string evbuf = to!string(unctrl(ev)); 563 KeyboardEvent ret = KeyboardEvent(Keycode.Unknown, InputModifier.None); 564 565 if (evbuf.length == 1 || (evbuf.length > 1 && evbuf[0] != '^')) 566 { 567 if (isAlpha(evbuf[0])) 568 { 569 string parsestr = to!string(toUpper(evbuf[0])); 570 ret.key = parse!Keycode(parsestr); 571 } 572 input.buf ~= evbuf; 573 } 574 else if (evbuf == "^[") 575 { 576 ret.mods |= InputModifier.Alt; 577 evbuf ~= to!string(unctrl(getch())); 578 if (evbuf.length == 4 && 579 evbuf[2] == '^' && 580 isAlpha(evbuf[3])) 581 { 582 ret.mods |= InputModifier.Control; 583 string parsestr = to!string(evbuf[3]); 584 ret.key = parse!Keycode(parsestr); 585 } 586 else if (evbuf.length == 3 && isAlpha(evbuf[2])) 587 { 588 if (isUpper(evbuf[2])) 589 ret.mods |= InputModifier.Shift; 590 string parsestr = to!string(toUpper(evbuf[2])); 591 ret.key = parse!Keycode(parsestr); 592 } 593 } 594 else if (evbuf.length == 2 && 595 evbuf[0] == '^' && 596 isAlpha(evbuf[1])) 597 { 598 ret.mods |= InputModifier.Control; 599 string parsestr = to!string(evbuf[1]); 600 ret.key = parse!Keycode(parsestr); 601 } 602 return ret; 603 } 604 605 version (Posix) 606 { 607 public static bool terminalInFamily(string[] terms...) 608 { 609 auto term = environment.get("TERM"); 610 foreach (t; terms) 611 if (indexOf(term, t) != -1) 612 return true; 613 return false; 614 } 615 616 public static bool isMacTerminal() 617 { 618 auto term = environment.get("TERM"); 619 return term == "xterm-256color"; 620 } 621 } 622 623 struct Input 624 { 625 void delegate(immutable KeyboardEvent) keyPressedCallback; 626 string buffer() 627 { 628 string ret = buf; 629 buf.length = 0; 630 return ret; 631 } 632 633 dchar ch() 634 { 635 if (buf.length > 0) 636 { 637 dchar ret = buf[0]; 638 buf = buf[1 .. $]; 639 return ret; 640 } 641 else 642 return 0; 643 } 644 645 public bool isEnable = true; 646 private string buf; 647 } 648 649 struct Event 650 { 651 size_t eventsProcessed; 652 ubyte flags; 653 } 654 655 enum EventFlag : ubyte 656 { 657 None = 0, 658 RawBuffer = 1 << 0, 659 InputBuffer = 1 << 1, 660 Resize = 1 << 2, 661 } 662 663 public Input input; 664 665 private immutable OutputType outType; 666 private immutable int outputDescriptor; 667 private immutable int inputDescriptor; 668 private string buffer; 669 private bool rawMode = false; 670 671 version (Posix) 672 { 673 private termios origTermios; 674 } 675 else version (Windows) 676 { 677 private HANDLE hConsole; 678 private CONSOLE_SCREEN_BUFFER_INFO originalSbi; 679 } 680 }