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