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 }