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