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