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