1 module jcli.text.console; 2 3 import jansi, jcli.text; 4 5 import std.conv : to; 6 import std.stdio : stdout; 7 8 version(Windows) import core.sys.windows.windows; 9 version(Posix) import core.sys.posix.termios, core.sys.posix.unistd, core.sys.posix.signal; 10 11 enum ConsoleKey 12 { 13 unknown, 14 15 a, b, c, d, e, f, g, 16 h, i, j, k, l, m, n, 17 o, p, q, r, s, t, u, 18 v, w, x, y, z, 19 20 printScreen, scrollLock, pause, 21 insert, home, pageUp, 22 pageDown, del, end, 23 24 up, down, left, right, 25 26 escape, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, 27 28 enter, back, tab 29 } 30 31 struct ConsoleEventUnknown {} 32 struct ConsoleEventInterrupt {} 33 struct ConsoleKeyEvent 34 { 35 enum SpecialKey 36 { 37 capslock = 0x0080, 38 leftAlt = 0x0002, 39 leftCtrl = 0x0008, 40 numlock = 0x0020, 41 rightAlt = 0x0001, 42 rightCtrl = 0x0004, 43 scrolllock = 0x0040, 44 shift = 0x0010 45 } 46 47 bool isDown; 48 uint repeatCount; 49 ConsoleKey key; 50 uint scancode; 51 union 52 { 53 dchar charAsUnicode; 54 char charAsAscii; 55 } 56 SpecialKey specialKeys; 57 } 58 59 import std.sumtype; 60 61 alias ConsoleEvent = SumType!( 62 ConsoleKeyEvent, 63 ConsoleEventInterrupt, 64 ConsoleEventUnknown 65 ); 66 67 final class Console 68 { 69 static: 70 71 private bool _useAlternateBuffer; 72 private bool _wasControlC; 73 version(Windows) private 74 { 75 HANDLE _stdin = INVALID_HANDLE_VALUE; 76 DWORD _oldMode; 77 UINT _oldOutputCP; 78 UINT _oldInputCP; 79 } 80 version(Posix) private 81 { 82 bool _attached; 83 termios _oldIos; 84 } 85 86 bool attach(bool useAlternativeBuffer = true) 87 { 88 _useAlternateBuffer = false; 89 version(Windows) 90 { 91 Console._stdin = GetStdHandle(STD_INPUT_HANDLE); 92 if(Console._stdin == INVALID_HANDLE_VALUE) 93 return false; 94 95 if(!GetConsoleMode(Console._stdin, &Console._oldMode)) 96 return false; 97 98 if(!SetConsoleMode(Console._stdin, Console._oldMode | ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) 99 return false; 100 101 this._oldInputCP = GetConsoleCP(); 102 this._oldOutputCP = GetConsoleOutputCP(); 103 SetConsoleOutputCP(CP_UTF8); 104 SetConsoleCP(CP_UTF8); 105 106 SetConsoleCtrlHandler((ctrlType) 107 { 108 if(ctrlType == CTRL_C_EVENT) 109 { 110 Console._wasControlC = true; 111 return TRUE; 112 } 113 114 return FALSE; 115 }, TRUE); 116 117 if(_useAlternateBuffer) 118 stdout.write("\033[?1049h"); 119 return true; 120 } 121 else version(Posix) 122 { 123 if(_useAlternateBuffer) 124 stdout.write("\033[?1049h"); 125 tcgetattr(STDIN_FILENO, &_oldIos); 126 auto newIos = _oldIos; 127 128 newIos.c_lflag &= ~ECHO; 129 newIos.c_lflag &= ~ICANON; 130 newIos.c_cc[VMIN] = 0; 131 newIos.c_cc[VTIME] = 1; 132 133 tcsetattr(STDIN_FILENO, TCSAFLUSH, &newIos); 134 signal(SIGINT, (_){ Console._wasControlC = true; }); 135 136 this._attached = true; 137 return true; 138 } 139 else return false; 140 } 141 142 void detach() 143 { 144 version(Windows) 145 { 146 if(!Console.isAttached) 147 return; 148 149 SetConsoleMode(Console._stdin, Console._oldMode); 150 SetConsoleOutputCP(this._oldOutputCP); 151 SetConsoleCP(this._oldInputCP); 152 SetConsoleCtrlHandler(null, FALSE); 153 Console._stdin = INVALID_HANDLE_VALUE; 154 } 155 else version(Posix) 156 { 157 if(!Console.isAttached) 158 return; 159 this._attached = false; 160 tcsetattr(STDIN_FILENO, TCSAFLUSH, &_oldIos); 161 signal(SIGINT, SIG_DFL); 162 } 163 164 if(_useAlternateBuffer) 165 stdout.write("\033[?1049l"); 166 } 167 168 bool isAttached() 169 { 170 version(Windows) return Console._stdin != INVALID_HANDLE_VALUE; 171 else version(Posix) return Console._attached; 172 else return false; 173 } 174 175 void processEvents(void delegate(ConsoleEvent) handler) 176 { 177 assert(handler !is null, "A null handler was provided."); 178 179 if(_wasControlC) 180 { 181 _wasControlC = false; 182 handler(ConsoleEvent(ConsoleEventInterrupt())); 183 } 184 185 version(Windows) 186 { 187 INPUT_RECORD[8] events; 188 DWORD eventsRead; 189 190 assert(Console.isAttached, "We're not attached to the console."); 191 if(!PeekConsoleInput(Console._stdin, &events[0], 1, &eventsRead)) 192 return; 193 194 ReadConsoleInput(Console._stdin, &events[0], cast(DWORD)events.length, &eventsRead); 195 foreach(event; events[0..eventsRead]) 196 { 197 const e = Console.translateEvent(event); 198 handler(e); 199 } 200 } 201 else version(Posix) 202 { 203 import core.sys.posix.unistd : read; 204 205 char ch; 206 ssize_t bytesRead = read(STDIN_FILENO, &ch, 1); 207 while(bytesRead > 0 && Console.isAttached) 208 { 209 handler(ConsoleEvent(Console.translateKeyEvent(ch))); 210 211 if(Console.isAttached) 212 bytesRead = read(STDIN_FILENO, &ch, 1); 213 } 214 } 215 } 216 217 void waitForInput() 218 { 219 assert(Console.isAttached, "We're not attached to the console."); 220 version(Windows) 221 { 222 WaitForSingleObject(Console._stdin, 0); 223 } 224 } 225 226 void setCursor(uint x, uint y) 227 { 228 stdout.writef("\033[%s;%sH", y, x); 229 } 230 231 void hideCursor() 232 { 233 stdout.write("\033[?25l"); 234 } 235 236 void showCursor() 237 { 238 stdout.write("\033[?25h"); 239 } 240 241 Vector screenSize() 242 { 243 version(Windows) 244 { 245 CONSOLE_SCREEN_BUFFER_INFO csbi; 246 GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi); 247 248 return Vector( 249 csbi.srWindow.Right - csbi.srWindow.Left + 1, 250 csbi.srWindow.Bottom - csbi.srWindow.Top + 1 251 ); 252 } 253 else version(Posix) 254 { 255 import core.sys.posix.sys.ioctl, core.sys.posix.unistd, core.sys.posix.stdio; 256 winsize w; 257 ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); 258 return Vector(w.ws_col, w.ws_row); 259 } 260 else return Vector(80, 20); 261 } 262 263 void refreshHandler(uint row, const TextBufferCell[] rowCells) 264 { 265 import std.array; 266 static Appender!(char[]) builder; 267 268 Console.setCursor(0, row.to!uint + 1); 269 builder.clear(); 270 271 foreach(i, cell; rowCells) 272 { 273 if(i == 0 || cell.style != rowCells[i-1].style) 274 { 275 builder.put(ANSI_COLOUR_RESET); 276 char[AnsiStyleSet.MAX_CHARS_NEEDED] buffer; 277 builder.put(ANSI_CSI); 278 builder.put(cell.style.toSequence(buffer)); 279 builder.put(ANSI_COLOUR_END); 280 } 281 builder.put(cell.ch[0..cell.chLen]); 282 } 283 284 builder.put(ANSI_COLOUR_RESET); 285 stdout.write(builder.data); 286 } 287 288 TextBuffer createTextBuffer() 289 { 290 assert(this.isAttached, "We're not attached to the console."); 291 auto buffer = new TextBuffer(Console.screenSize.x, Console.screenSize.y); 292 import std.functional : toDelegate; 293 buffer.onRefresh((&Console.refreshHandler).toDelegate); 294 return buffer; 295 } 296 297 private version(Windows) 298 { 299 ConsoleEvent translateEvent(INPUT_RECORD event) 300 { 301 switch(event.EventType) 302 { 303 case KEY_EVENT: 304 const k = event.KeyEvent; 305 auto e = ConsoleKeyEvent( 306 cast(bool)k.bKeyDown, 307 k.wRepeatCount, 308 Console.translateKey(k.wVirtualKeyCode), 309 k.wVirtualScanCode, 310 ); 311 e.specialKeys = cast(ConsoleKeyEvent.SpecialKey)k.dwControlKeyState; 312 e.charAsUnicode = k.UnicodeChar.to!dchar; 313 314 return ConsoleEvent(e); 315 316 default: return ConsoleEvent(ConsoleEventUnknown()); 317 } 318 } 319 320 // https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes 321 ConsoleKey translateKey(uint keycode) 322 { 323 switch(keycode) with(ConsoleKey) 324 { 325 case VK_TAB: return ConsoleKey.tab; 326 case VK_SNAPSHOT: return ConsoleKey.printScreen; 327 case VK_SCROLL: return ConsoleKey.scrollLock; 328 case VK_PAUSE: return ConsoleKey.pause; 329 case VK_INSERT: return ConsoleKey.insert; 330 case VK_HOME: return ConsoleKey.home; 331 case VK_DELETE: return ConsoleKey.del; 332 case VK_END: return ConsoleKey.end; 333 case VK_NEXT: return ConsoleKey.pageDown; 334 case VK_PRIOR: return ConsoleKey.pageUp; 335 336 case VK_ESCAPE: return ConsoleKey.escape; 337 case VK_F1:..case VK_F12: 338 return cast(ConsoleKey)(cast(uint)ConsoleKey.f1 + (keycode - VK_F1)); 339 340 case VK_RETURN: return ConsoleKey.enter; 341 case VK_BACK: return ConsoleKey.back; 342 343 case VK_UP: return ConsoleKey.up; 344 case VK_DOWN: return ConsoleKey.down; 345 case VK_LEFT: return ConsoleKey.left; 346 case VK_RIGHT: return ConsoleKey.right; 347 348 // a-z 349 case 0x41:..case 0x5A: 350 return cast(ConsoleKey)(cast(uint)ConsoleKey.a + (keycode - 0x41)); 351 352 default: return unknown; 353 } 354 } 355 } 356 357 private version(Posix) 358 { 359 ConsoleKeyEvent translateKeyEvent(char ch) 360 { 361 ConsoleKeyEvent event; 362 event.key = Console.translateKey(ch, event.charAsUnicode); 363 event.isDown = true; 364 event.charAsAscii = ch; 365 366 return event; 367 } 368 369 ConsoleKey translateKey(char firstCh, out dchar utf) 370 { 371 import core.sys.posix.unistd : read; 372 373 // TODO: Unicode support. 374 switch(firstCh) with(ConsoleKey) 375 { 376 case 0x1A: return ConsoleKey.pause; 377 case '\t': return ConsoleKey.tab; 378 379 case 0x0A: return ConsoleKey.enter; 380 case 0x7F: return ConsoleKey.back; 381 382 // a-z 383 case 0x41:..case 0x5A: 384 utf = firstCh; 385 return cast(ConsoleKey)(cast(uint)ConsoleKey.a + (firstCh - 0x41)); 386 387 case '\033': 388 char ch; 389 auto bytesRead = read(STDIN_FILENO, &ch, 1); 390 if(bytesRead == 0) 391 return escape; 392 else if(ch == 'O' && read(STDIN_FILENO, &ch, 1) != 0 && ch >= 0x50 && ch <= 0x7E) 393 return cast(ConsoleKey)(cast(uint)ConsoleKey.f1 + (ch - 0x50)); 394 else if(ch != '[') 395 return unknown; 396 397 bytesRead = read(STDIN_FILENO, &ch, 1); 398 if(bytesRead == 0) 399 return unknown; 400 401 switch(ch) 402 { 403 case 'A': return ConsoleKey.up; 404 case 'B': return ConsoleKey.down; 405 case 'C': return ConsoleKey.right; 406 case 'D': return ConsoleKey.left; 407 case 'H': return ConsoleKey.home; 408 case 'F': return ConsoleKey.end; 409 case '2': 410 bytesRead = read(STDIN_FILENO, &ch, 1); 411 if(bytesRead == 0 || ch != '~') 412 return unknown; 413 return ConsoleKey.insert; 414 case '3': 415 bytesRead = read(STDIN_FILENO, &ch, 1); 416 if(bytesRead == 0 || ch != '~') 417 return unknown; 418 return ConsoleKey.del; 419 case '5': 420 bytesRead = read(STDIN_FILENO, &ch, 1); 421 if(bytesRead == 0 || ch != '~') 422 return unknown; 423 return ConsoleKey.pageUp; 424 case '6': 425 bytesRead = read(STDIN_FILENO, &ch, 1); 426 if(bytesRead == 0 || ch != '~') 427 return unknown; 428 return ConsoleKey.pageDown; 429 430 431 432 default: return unknown; 433 } 434 435 default: return unknown; 436 } 437 } 438 } 439 }