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