zymosis: cosmetix
[iv.d.git] / rawtty.d
blob75dc453d4b79d121197b04d2d490102f3f11bc5b
1 /* Invisible Vector Library
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 // linux tty utilities
19 module iv.rawtty /*is aliced*/;
21 import iv.alice;
22 import core.sys.posix.termios : termios;
23 import iv.strex : strEquCI;
24 import iv.utfutil;
26 public import iv.termrgb;
29 // ////////////////////////////////////////////////////////////////////////// //
30 alias TtyRgb2Color = TtyRGB;
31 alias ttyRgb2Color = ttyRGB;
34 // ////////////////////////////////////////////////////////////////////////// //
35 private __gshared termios origMode;
36 private __gshared bool doRestoreOrig = false;
37 private shared bool inRawMode = false;
39 private class XLock {}
42 // ////////////////////////////////////////////////////////////////////////// //
43 /// TTY mode
44 enum TTYMode {
45 Bad = -1, /// some error occured
46 Normal, /// normal ('cooked') mode
47 Raw /// 'raw' mode
51 __gshared bool xtermMetaSendsEscape = true; /// you should add `XTerm*metaSendsEscape: true` to "~/.Xdefaults"
52 private __gshared bool ttyIsFuckedFlag = false;
55 // ////////////////////////////////////////////////////////////////////////// //
56 /// is TTY fucked with utfuck?
57 @property bool ttyIsUtfucked () nothrow @trusted @nogc { pragma(inline, true); return ttyIsFuckedFlag; }
60 // ////////////////////////////////////////////////////////////////////////// //
61 /// return TTY width
62 @property int ttyWidth () nothrow @trusted @nogc {
63 if (!ttyIsRedirected) {
64 import core.sys.posix.sys.ioctl : ioctl, winsize, TIOCGWINSZ;
65 winsize sz = void;
66 if (ioctl(1, TIOCGWINSZ, &sz) != -1) return sz.ws_col;
68 return 80;
72 /// return TTY height
73 @property int ttyHeight () nothrow @trusted @nogc {
74 if (!ttyIsRedirected) {
75 import core.sys.posix.sys.ioctl : ioctl, winsize, TIOCGWINSZ;
76 winsize sz = void;
77 if (ioctl(1, TIOCGWINSZ, &sz) != -1) return sz.ws_row;
78 return sz.ws_row;
80 return 25;
84 // ////////////////////////////////////////////////////////////////////////// //
85 void ttyRawWrite (const(char)[] str...) nothrow @trusted @nogc {
86 import core.sys.posix.unistd : write;
87 if (str.length) write(1, str.ptr, str.length);
91 void ttyRawWriteInt(T) (T n) nothrow @trusted @nogc if (__traits(isIntegral, T) && !is(T == char) && !is(T == wchar) && !is(T == dchar) && !is(T == bool) && !is(T == enum)) {
92 import core.stdc.stdio : snprintf;
93 import core.sys.posix.unistd : write;
94 char[64] buf = void;
95 static if (__traits(isUnsigned, T)) {
96 static if (T.sizeof > 4) {
97 auto len = snprintf(buf.ptr, buf.length, "%llu", n);
98 } else {
99 auto len = snprintf(buf.ptr, buf.length, "%u", cast(uint)n);
101 } else {
102 static if (T.sizeof > 4) {
103 auto len = snprintf(buf.ptr, buf.length, "%lld", n);
104 } else {
105 auto len = snprintf(buf.ptr, buf.length, "%d", cast(int)n);
108 if (len > 0) write(1, buf.ptr, len);
112 void ttyBeep () nothrow @trusted @nogc {
113 import core.sys.posix.unistd : write;
114 enum str = "\x07";
115 write(1, str.ptr, str.length);
119 void ttyEnableBracketedPaste () nothrow @trusted @nogc {
120 import core.sys.posix.unistd : write;
121 enum str = "\x1b[?2004h";
122 write(1, str.ptr, str.length);
126 void ttyDisableBracketedPaste () nothrow @trusted @nogc {
127 import core.sys.posix.unistd : write;
128 enum str = "\x1b[?2004l";
129 write(1, str.ptr, str.length);
133 void ttyEnableFocusReports () nothrow @trusted @nogc {
134 import core.sys.posix.unistd : write;
135 enum str = "\x1b[?1004h";
136 write(1, str.ptr, str.length);
140 void ttyDisableFocusReports () nothrow @trusted @nogc {
141 import core.sys.posix.unistd : write;
142 enum str = "\x1b[?1004l";
143 write(1, str.ptr, str.length);
147 void ttyEnableMouseReports () nothrow @trusted @nogc {
148 import core.sys.posix.unistd : write;
149 enum str = "\x1b[?1000h\x1b[?1006h\x1b[?1002h";
150 write(1, str.ptr, str.length);
154 void ttyDisableMouseReports () nothrow @trusted @nogc {
155 import core.sys.posix.unistd : write;
156 enum str = "\x1b[?1002l\x1b[?1006l\x1b[?1000l";
157 write(1, str.ptr, str.length);
161 // ////////////////////////////////////////////////////////////////////////// //
162 /// get current TTY mode
163 TTYMode ttyGetMode () nothrow @trusted @nogc {
164 import core.atomic;
165 return (atomicLoad(inRawMode) ? TTYMode.Raw : TTYMode.Normal);
169 /// Restore terminal mode we had at program startup
170 void ttyRestoreOrigMode () {
171 import core.atomic;
172 import core.sys.posix.termios : tcflush, tcsetattr;
173 import core.sys.posix.termios : TCIOFLUSH, TCSAFLUSH;
174 import core.sys.posix.unistd : STDIN_FILENO;
175 //tcflush(STDIN_FILENO, TCIOFLUSH);
176 if (doRestoreOrig) tcsetattr(STDIN_FILENO, TCSAFLUSH, &origMode);
177 atomicStore(inRawMode, false);
181 /// returns previous mode or Bad
182 TTYMode ttySetNormal () @trusted @nogc {
183 import core.atomic;
184 if (!doRestoreOrig) return TTYMode.Bad;
185 synchronized(XLock.classinfo) {
186 if (atomicLoad(inRawMode)) {
187 import core.sys.posix.termios : tcflush, tcsetattr;
188 import core.sys.posix.termios : TCIOFLUSH, TCSAFLUSH;
189 import core.sys.posix.unistd : STDIN_FILENO;
190 //tcflush(STDIN_FILENO, TCIOFLUSH);
191 if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &origMode) < 0) return TTYMode.Bad;
192 atomicStore(inRawMode, false);
193 return TTYMode.Raw;
195 return TTYMode.Normal;
200 /// returns previous mode or Bad
201 TTYMode ttySetRaw (bool waitkey=true) @trusted @nogc {
202 import core.atomic;
203 if (ttyIsRedirected || !doRestoreOrig) return TTYMode.Bad;
204 synchronized(XLock.classinfo) {
205 if (!atomicLoad(inRawMode)) {
206 //import core.sys.posix.termios : tcflush, tcsetattr;
207 //import core.sys.posix.termios : TCIOFLUSH, TCSAFLUSH;
208 //import core.sys.posix.termios : BRKINT, CS8, ECHO, ICANON, IEXTEN, INPCK, ISIG, ISTRIP, IXOFF, IGNCR, INLCR, ONLCR, OPOST, VMIN, VTIME;
209 import core.sys.posix.termios;
210 import core.sys.posix.unistd : STDIN_FILENO;
211 import core.stdc.string : memset;
212 enum IUCLC = 512; //0001000
213 //termios raw = origMode; // modify the original mode
214 termios raw = void;
215 memset(&raw, 0, raw.sizeof);
216 //tcflush(STDIN_FILENO, TCIOFLUSH);
217 raw.c_iflag = IGNBRK;
218 // output modes: disable post processing
219 raw.c_oflag = OPOST|ONLCR;
220 // control modes: set 8 bit chars
221 raw.c_cflag = CS8|CLOCAL;
222 // control chars: set return condition: min number of bytes and timer; we want read to return every single byte, without timeout
223 raw.c_cc[VMIN] = (waitkey ? 1 : 0); // wait/poll mode
224 raw.c_cc[VTIME] = 0; // no timer
225 // put terminal in raw mode after flushing
226 if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) < 0) return TTYMode.Bad;
228 import core.sys.posix.unistd : write;
229 // G0 is ASCII, G1 is graphics
230 enum setupStr = "\x1b(B\x1b)0\x0f";
231 write(1, setupStr.ptr, setupStr.length);
233 atomicStore(inRawMode, true);
234 return TTYMode.Normal;
236 return TTYMode.Raw;
241 /// change TTY mode if possible
242 /// returns previous mode or Bad
243 TTYMode ttySetMode (TTYMode mode) @trusted @nogc {
244 // check what we can without locking
245 if (mode == TTYMode.Bad) return TTYMode.Bad;
246 if (ttyIsRedirected) return (mode == TTYMode.Normal ? TTYMode.Normal : TTYMode.Bad);
247 synchronized(XLock.classinfo) return (mode == TTYMode.Normal ? ttySetNormal() : ttySetRaw());
251 // ////////////////////////////////////////////////////////////////////////// //
252 /// set wait/poll mode
253 bool ttySetWaitKey (bool doWait) @trusted @nogc {
254 import core.atomic;
255 if (ttyIsRedirected) return false;
256 synchronized(XLock.classinfo) {
257 if (atomicLoad(inRawMode)) {
258 import core.sys.posix.termios : tcflush, tcgetattr, tcsetattr;
259 import core.sys.posix.termios : TCIOFLUSH, TCSAFLUSH;
260 import core.sys.posix.termios : VMIN;
261 import core.sys.posix.unistd : STDIN_FILENO;
262 termios raw;
263 //tcflush(STDIN_FILENO, TCIOFLUSH);
264 if (tcgetattr(STDIN_FILENO, &raw) != 0) return false; //redirected = false;
265 raw.c_cc[VMIN] = (doWait ? 1 : 0); // wait/poll mode
266 if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) < 0) return false;
267 return true;
270 return false;
274 // ////////////////////////////////////////////////////////////////////////// //
276 * Wait for keypress.
278 * Params:
279 * toMSec = timeout in milliseconds; <0: infinite; 0: don't wait; default is -1
281 * Returns:
282 * true if key was pressed, false if no key was pressed in the given time
284 bool ttyWaitKey (int toMSec=-1) @trusted @nogc {
285 import core.atomic;
286 if (!ttyIsRedirected && atomicLoad(inRawMode)) {
287 import core.sys.posix.sys.select : fd_set, select, timeval, FD_ISSET, FD_SET, FD_ZERO;
288 import core.sys.posix.unistd : STDIN_FILENO;
289 timeval tv;
290 fd_set fds;
291 FD_ZERO(&fds);
292 FD_SET(STDIN_FILENO, &fds); //STDIN_FILENO is 0
293 if (toMSec <= 0) {
294 tv.tv_sec = 0;
295 tv.tv_usec = 0;
296 } else {
297 tv.tv_sec = cast(int)(toMSec/1000);
298 tv.tv_usec = (toMSec%1000)*1000;
300 select(STDIN_FILENO+1, &fds, null, null, (toMSec < 0 ? null : &tv));
301 return FD_ISSET(STDIN_FILENO, &fds);
303 return false;
308 * Check if key was pressed. Don't block.
310 * Returns:
311 * true if key was pressed, false if no key was pressed
313 bool ttyIsKeyHit () @trusted @nogc { return ttyWaitKey(0); }
317 * Read one byte from stdin.
319 * Params:
320 * toMSec = timeout in milliseconds; <0: infinite; 0: don't wait; default is -1
322 * Returns:
323 * read byte or -1 on error/timeout
325 int ttyReadKeyByte (int toMSec=-1) @trusted @nogc {
326 import core.atomic;
327 if (!ttyIsRedirected && atomicLoad(inRawMode)) {
328 import core.sys.posix.unistd : read, STDIN_FILENO;
329 ubyte res;
330 if (toMSec >= 0) {
331 synchronized(XLock.classinfo) if (ttyWaitKey(toMSec) && read(STDIN_FILENO, &res, 1) == 1) return res;
332 } else {
333 if (read(STDIN_FILENO, &res, 1) == 1) {
334 //{ import core.stdc.stdio; if (res > 32 && res != 127) printf("[%c]\n", res); else printf("{%d}\n", res); }
335 return res;
339 return -1;
343 // ////////////////////////////////////////////////////////////////////////// //
344 /// pressed key info
345 public align(1) struct TtyEvent {
346 align(1): // make it tightly packed
348 enum Key : ubyte{
349 None, ///
350 Error, /// error reading key
351 Unknown, /// can't interpret escape code
353 Char, ///
355 // for bracketed paste mode
356 PasteStart,
357 PasteEnd,
359 ModChar, /// char with some modifier
361 Up, ///
362 Down, ///
363 Left, ///
364 Right, ///
365 Insert, ///
366 Delete, ///
367 PageUp, ///
368 PageDown, ///
369 Home, ///
370 End, ///
372 Escape, ///
373 Backspace, ///
374 Tab, ///
375 Enter, ///
377 Pad5, /// xterm can return this
379 F1, ///
380 F2, ///
381 F3, ///
382 F4, ///
383 F5, ///
384 F6, ///
385 F7, ///
386 F8, ///
387 F9, ///
388 F10, ///
389 F11, ///
390 F12, ///
392 //A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
393 //N0, N1, N2, N3, N4, N5, N6, N7, N8, N9,
395 MLeftDown, ///
396 MLeftUp, ///
397 MLeftMotion, ///
398 MMiddleDown, ///
399 MMiddleUp, ///
400 MMiddleMotion, ///
401 MRightDown, ///
402 MRightUp, ///
403 MRightMotion, ///
405 MWheelUp, ///
406 MWheelDown, ///
408 MMotion, /// mouse motion without buttons, not read from tty by now, but can be useful for other backends
410 // synthesized events, used in tui
411 MLeftClick, ///
412 MMiddleClick, ///
413 MRightClick, ///
415 MLeftDouble, ///
416 MMiddleDouble, ///
417 MRightDouble, ///
419 FocusIn, ///
420 FocusOut, ///
424 enum ModFlag : ubyte {
425 Ctrl = 1<<0, ///
426 Alt = 1<<1, ///
427 Shift = 1<<2, ///
431 enum MButton : int {
432 None = 0, ///
433 Left = 1, ///
434 Middle = 2, ///
435 Right = 3, ///
436 WheelUp = 4, ///
437 WheelDown = 5, ///
438 First = Left, ///
441 Key key; /// key type/sym
442 ubyte mods; /// set of ModFlag
443 dchar ch = 0; /// can be 0 for special key
444 short x, y; /// for mouse reports
445 //void* udata; /// arbitrary user data
447 @property const pure nothrow @safe @nogc {
449 MButton button () { pragma(inline, true); return
450 key == Key.MLeftDown || key == Key.MLeftUp || key == Key.MLeftMotion || key == Key.MLeftClick || key == Key.MLeftDouble ? MButton.Left :
451 key == Key.MRightDown || key == Key.MRightUp || key == Key.MRightMotion || key == Key.MRightClick || key == Key.MRightDouble ? MButton.Right :
452 key == Key.MMiddleDown || key == Key.MMiddleUp || key == Key.MMiddleMotion || key == Key.MMiddleClick || key == Key.MMiddleDouble ? MButton.Middle :
453 key == Key.MWheelUp ? MButton.WheelUp :
454 key == Key.MWheelDown ? MButton.WheelDown :
455 MButton.None;
457 bool mouse () { pragma(inline, true); return (key >= Key.MLeftDown && key <= Key.MRightDouble); } ///
458 bool mpress () { pragma(inline, true); return (key == Key.MLeftDown || key == Key.MRightDown || key == Key.MMiddleDown); } ///
459 bool mrelease () { pragma(inline, true); return (key == Key.MLeftUp || key == Key.MRightUp || key == Key.MMiddleUp); } ///
460 bool mclick () { pragma(inline, true); return (key == Key.MLeftClick || key == Key.MRightClick || key == Key.MMiddleClick); } ///
461 bool mdouble () { pragma(inline, true); return (key == Key.MLeftDouble || key == Key.MRightDouble || key == Key.MMiddleDouble); } ///
462 bool mmotion () { pragma(inline, true); return (key == Key.MLeftMotion || key == Key.MRightMotion || key == Key.MMiddleMotion || key == Key.MMotion); } ///
463 bool mwheel () { pragma(inline, true); return (key == Key.MWheelUp || key == Key.MWheelDown); } ///
464 bool focusin () { pragma(inline, true); return (key == Key.FocusIn); } ///
465 bool focusout () { pragma(inline, true); return (key == Key.FocusOut); } ///
466 bool ctrl () { pragma(inline, true); return ((mods&ModFlag.Ctrl) != 0); } ///
467 bool alt () { pragma(inline, true); return ((mods&ModFlag.Alt) != 0); } ///
468 bool shift () { pragma(inline, true); return ((mods&ModFlag.Shift) != 0); } ///
471 @property pure nothrow @safe @nogc {
472 void ctrl (bool v) { pragma(inline, true); if (v) mods |= ModFlag.Ctrl; else mods &= ~(ModFlag.Ctrl); } ///
473 void alt (bool v) { pragma(inline, true); if (v) mods |= ModFlag.Alt; else mods &= ~(ModFlag.Alt); } ///
474 void shift (bool v) { pragma(inline, true); if (v) mods |= ModFlag.Shift; else mods &= ~(ModFlag.Shift); } ///
477 this (const(char)[] s) pure nothrow @safe @nogc {
478 if (TtyEvent.parse(this, s).length != 0) {
479 key = Key.Error;
480 mods = 0;
481 ch = 0;
485 bool opEquals (in TtyEvent k) const pure nothrow @safe @nogc {
486 pragma(inline, true);
487 return
488 (key == k.key ?
489 (key == Key.Char ? (ch == k.ch) :
490 key == Key.ModChar ? (mods == k.mods && ch == k.ch) :
491 //key >= Key.MLeftDown && key <= MWheelDown ? true :
492 key > Key.ModChar ? (mods == k.mods) :
493 true
494 ) : false
498 bool opEquals (const(char)[] s) const pure nothrow @safe @nogc {
499 TtyEvent k;
500 if (TtyEvent.parse(k, s).length != 0) return false;
501 return (k == this);
505 string toString () const nothrow {
506 char[128] buf = void;
507 return toCharBuf(buf[]).idup;
511 char[] toCharBuf (char[] dest) const nothrow @trusted @nogc {
512 static immutable string hexD = "0123456789abcdef";
513 int dpos = 0;
514 void put (const(char)[] s...) nothrow @nogc {
515 foreach (char ch; s) {
516 if (dpos >= dest.length) break;
517 dest.ptr[dpos++] = ch;
520 void putMods () nothrow @nogc {
521 if (ctrl) put("C-");
522 if (alt) put("M-");
523 if (shift) put("S-");
525 if (key == Key.ModChar) putMods();
526 if (key == Key.Char || key == Key.ModChar) {
527 if (ch < ' ' || ch == 127) {
528 put("x");
529 put(hexD.ptr[(ch>>4)&0x0f]);
530 put(hexD.ptr[ch&0x0f]);
531 } else if (ch == ' ') {
532 put("space");
533 } else if (ch < 256) {
534 put(cast(char)ch);
535 } else if (ch <= 0xffff) {
536 put("u");
537 put(hexD.ptr[(ch>>12)&0x0f]);
538 put(hexD.ptr[(ch>>8)&0x0f]);
539 put(hexD.ptr[(ch>>4)&0x0f]);
540 put(hexD.ptr[ch&0x0f]);
541 } else {
542 put("error");
544 return dest[0..dpos];
546 if (key == Key.None) { put("none"); return dest[0..dpos]; }
547 if (key == Key.Error) { put("error"); return dest[0..dpos]; }
548 if (key == Key.Unknown) { put("unknown"); return dest[0..dpos]; }
549 foreach (string kn; __traits(allMembers, TtyEvent.Key)) {
550 if (__traits(getMember, TtyEvent.Key, kn) == key) {
551 putMods();
552 put(kn);
553 return dest[0..dpos];
556 put("error");
557 return dest[0..dpos];
560 /** parse key name. get first word, return rest of the string (with trailing spaces removed)
562 * "C-<home>" (emacs-like syntax is recognized)
564 * "C-M-x"
566 * mods: C(trl), M(eta:alt), S(hift)
568 * `key` will be `TtyEvent.Key.Error` on error, `TtyEvent.Key.None` on empty string
570 static T parse(T) (out TtyEvent key, T s) pure nothrow @trusted @nogc if (is(T : const(char)[])) {
571 static if (is(T == typeof(null))) {
572 return null;
573 } else {
574 while (s.length && s.ptr[0] <= ' ') s = s[1..$];
575 if (s.length == 0) return s; // no more
576 // get space-delimited word
577 int pos = 1; // 0 is always non-space here
578 while (pos < s.length && s.ptr[pos] > ' ') { if (++pos >= 1024) return s; }
579 auto olds = s; // return this in case of error
580 const(char)[] str = s[0..pos]; // string to parse
581 // `s` will be our result; remove leading spaces for convenience
582 while (pos < s.length && s.ptr[pos] <= ' ') ++pos;
583 s = s[pos..$];
584 // parse word
585 while (str.length > 0) {
586 if (str.length >= 2 && str.ptr[1] == '-') {
587 // modifier
588 switch (str.ptr[0]) {
589 case 'C': case 'c': key.ctrl = true; break;
590 case 'M': case 'm': key.alt = true; break;
591 case 'S': case 's': key.shift = true; break;
592 default: goto error; // unknown modifier
594 str = str[2..$];
595 } else {
596 // key
597 if (str.length > 1 && str.ptr[0] == '^') {
598 // ^A means C-A
599 key.ctrl = true;
600 str = str[1..$];
601 } else if (str.length > 2 && str.ptr[0] == '<' && str[$-1] == '>') {
602 str = str[1..$-1];
604 if (str.length == 0) goto error; // just in case
605 if (str.strEquCI("space")) str = " ";
606 if (str.length == 1) {
607 // single char
608 key.ch = str.ptr[0];
609 if (key.ctrl || key.alt) {
610 key.key = TtyEvent.Key.ModChar;
611 if (key.ch >= 'a' && key.ch <= 'z') key.ch -= 32; // toupper
612 } else {
613 key.key = TtyEvent.Key.Char;
614 if (key.shift) {
615 if (key.ch >= 'a' && key.ch <= 'z') key.ch -= 32; // toupper
616 else switch (key.ch) {
617 case '`': key.ch = '~'; break;
618 case '1': key.ch = '!'; break;
619 case '2': key.ch = '@'; break;
620 case '3': key.ch = '#'; break;
621 case '4': key.ch = '$'; break;
622 case '5': key.ch = '%'; break;
623 case '6': key.ch = '^'; break;
624 case '7': key.ch = '&'; break;
625 case '8': key.ch = '*'; break;
626 case '9': key.ch = '('; break;
627 case '0': key.ch = ')'; break;
628 case '-': key.ch = '_'; break;
629 case '=': key.ch = '+'; break;
630 case '[': key.ch = '{'; break;
631 case ']': key.ch = '}'; break;
632 case ';': key.ch = ':'; break;
633 case '\'': key.ch = '"'; break;
634 case '\\': key.ch = '|'; break;
635 case ',': key.ch = '<'; break;
636 case '.': key.ch = '>'; break;
637 case '/': key.ch = '?'; break;
638 default:
640 key.shift = false;
643 } else {
644 // key name
645 if (str.strEquCI("return")) str = "enter";
646 if (str.strEquCI("esc")) str = "escape";
647 if (str.strEquCI("bs")) str = "backspace";
648 if (str.strEquCI("PasteStart") || str.strEquCI("Paste-Start")) {
649 key.key = TtyEvent.Key.PasteStart;
650 key.mods = 0;
651 key.ch = 0;
652 } else if (str.strEquCI("PasteEnd") || str.strEquCI("Paste-End")) {
653 key.key = TtyEvent.Key.PasteEnd;
654 key.mods = 0;
655 key.ch = 0;
656 } else {
657 bool found = false;
658 foreach (string kn; __traits(allMembers, TtyEvent.Key)) {
659 if (!found && str.strEquCI(kn)) {
660 found = true;
661 key.key = __traits(getMember, TtyEvent.Key, kn);
662 break;
665 if (!found || key.key < TtyEvent.Key.Up) goto error;
667 // just in case
668 if (key.key == TtyEvent.Key.Enter) key.ch = 13;
669 else if (key.key == TtyEvent.Key.Tab) key.ch = 9;
670 else if (key.key == TtyEvent.Key.Escape) key.ch = 27;
671 else if (key.key == TtyEvent.Key.Backspace) key.ch = 8;
673 return s;
676 error:
677 key = TtyEvent.init;
678 key.key = TtyEvent.Key.Error;
679 return olds;
686 * Read key from stdin.
688 * WARNING! no utf-8 support yet!
690 * Params:
691 * toMSec = timeout in milliseconds; <0: infinite; 0: don't wait; default is -1
692 * toEscMSec = timeout in milliseconds for escape sequences
694 * Returns:
695 * null on error or keyname
697 TtyEvent ttyReadKey (int toMSec=-1, int toEscMSec=-1/*300*/) @trusted @nogc {
698 TtyEvent key;
700 void skipCSI () @nogc {
701 key.key = TtyEvent.Key.Unknown;
702 for (;;) {
703 auto ch = ttyReadKeyByte(toEscMSec);
704 if (ch < 0 || ch == 27) { key.key = TtyEvent.Key.Escape; key.ch = 27; break; }
705 if (ch != ';' && (ch < '0' || ch > '9')) break;
709 void badCSI () @nogc {
710 key = key.init;
711 key.key = TtyEvent.Key.Unknown;
714 bool xtermMods (uint mci) @nogc {
715 switch (mci) {
716 case 2: key.shift = true; return true;
717 case 3: key.alt = true; return true;
718 case 4: key.alt = true; key.shift = true; return true;
719 case 5: key.ctrl = true; return true;
720 case 6: key.ctrl = true; key.shift = true; return true;
721 case 7: key.alt = true; key.ctrl = true; return true;
722 case 8: key.alt = true; key.ctrl = true; key.shift = true; return true;
723 default:
725 return false;
728 void xtermSpecial (char ch) @nogc {
729 switch (ch) {
730 case 'A': key.key = TtyEvent.Key.Up; break;
731 case 'B': key.key = TtyEvent.Key.Down; break;
732 case 'C': key.key = TtyEvent.Key.Right; break;
733 case 'D': key.key = TtyEvent.Key.Left; break;
734 case 'E': key.key = TtyEvent.Key.Pad5; break;
735 case 'H': key.key = TtyEvent.Key.Home; break;
736 case 'F': key.key = TtyEvent.Key.End; break;
737 case 'P': key.key = TtyEvent.Key.F1; break;
738 case 'Q': key.key = TtyEvent.Key.F2; break;
739 case 'R': key.key = TtyEvent.Key.F3; break;
740 case 'S': key.key = TtyEvent.Key.F4; break;
741 case 'Z': key.key = TtyEvent.Key.Tab; key.ch = 9; if (!key.shift && !key.alt && !key.ctrl) key.shift = true; break;
742 default: badCSI(); break;
746 void linconSpecial (char ch) @nogc {
747 switch (ch) {
748 case 'A': key.key = TtyEvent.Key.F1; break;
749 case 'B': key.key = TtyEvent.Key.F2; break;
750 case 'C': key.key = TtyEvent.Key.F3; break;
751 case 'D': key.key = TtyEvent.Key.F4; break;
752 default: badCSI(); break;
756 void csiSpecial (uint n) @nogc {
757 switch (n) {
758 case 1: key.key = TtyEvent.Key.Home; return; // xterm
759 case 2: key.key = TtyEvent.Key.Insert; return;
760 case 3: key.key = TtyEvent.Key.Delete; return;
761 case 4: key.key = TtyEvent.Key.End; return;
762 case 5: key.key = TtyEvent.Key.PageUp; return;
763 case 6: key.key = TtyEvent.Key.PageDown; return;
764 case 7: key.key = TtyEvent.Key.Home; return; // rxvt
765 case 8: key.key = TtyEvent.Key.End; return;
766 case 1+10: key.key = TtyEvent.Key.F1; return;
767 case 2+10: key.key = TtyEvent.Key.F2; return;
768 case 3+10: key.key = TtyEvent.Key.F3; return;
769 case 4+10: key.key = TtyEvent.Key.F4; return;
770 case 5+10: key.key = TtyEvent.Key.F5; return;
771 case 6+11: key.key = TtyEvent.Key.F6; return;
772 case 7+11: key.key = TtyEvent.Key.F7; return;
773 case 8+11: key.key = TtyEvent.Key.F8; return;
774 case 9+11: key.key = TtyEvent.Key.F9; return;
775 case 10+11: key.key = TtyEvent.Key.F10; return;
776 case 11+12: key.key = TtyEvent.Key.F11; return;
777 case 12+12: key.key = TtyEvent.Key.F12; return;
778 default: badCSI(); break;
782 // {\e}[<0;58;32M (button;x;y;[Mm])
783 void parseMouse () @nogc {
784 uint[3] nn;
785 uint nc = 0;
786 bool press = false;
787 for (;;) {
788 auto ch = ttyReadKeyByte(toEscMSec);
789 if (ch < 0 || ch == 27) { key.key = TtyEvent.Key.Escape; key.ch = 27; return; }
790 if (ch == ';') {
791 ++nc;
792 } else if (ch >= '0' && ch <= '9') {
793 if (nc < nn.length) nn.ptr[nc] = nn.ptr[nc]*10+ch-'0';
794 } else {
795 if (ch == 'M') press = true;
796 else if (ch == 'm') press = false;
797 else { key.key = TtyEvent.Key.Unknown; return; }
798 break;
801 if (nn[1] > 0) --nn[1];
802 if (nn[2] > 0) --nn[2];
803 if (nn[1] < 0) nn[1] = 1;
804 if (nn[1] > short.max) nn[1] = short.max;
805 if (nn[2] < 0) nn[2] = 1;
806 if (nn[2] > short.max) nn[2] = short.max;
807 switch (nn[0]) {
808 case 0: key.key = (press ? TtyEvent.Key.MLeftDown : TtyEvent.Key.MLeftUp); break;
809 case 1: key.key = (press ? TtyEvent.Key.MMiddleDown : TtyEvent.Key.MMiddleUp); break;
810 case 2: key.key = (press ? TtyEvent.Key.MRightDown : TtyEvent.Key.MRightUp); break;
811 case 32: if (!press) { key.key = TtyEvent.Key.Unknown; return; } key.key = TtyEvent.Key.MLeftMotion; break;
812 case 33: if (!press) { key.key = TtyEvent.Key.Unknown; return; } key.key = TtyEvent.Key.MMiddleMotion; break;
813 case 34: if (!press) { key.key = TtyEvent.Key.Unknown; return; } key.key = TtyEvent.Key.MRightMotion; break;
814 case 64: if (!press) { key.key = TtyEvent.Key.Unknown; return; } key.key = TtyEvent.Key.MWheelUp; break;
815 case 65: if (!press) { key.key = TtyEvent.Key.Unknown; return; } key.key = TtyEvent.Key.MWheelDown; break;
816 default: key.key = TtyEvent.Key.Unknown; return;
818 key.x = cast(short)nn[1];
819 key.y = cast(short)nn[2];
822 int ch = ttyReadKeyByte(toMSec);
823 if (ch < 0) { key.key = TtyEvent.Key.Error; return key; } // error
824 if (ch == 0) { key.key = TtyEvent.Key.ModChar; key.ctrl = true; key.ch = ' '; return key; }
825 if (ch == 8 || ch == 127) { key.key = TtyEvent.Key.Backspace; key.ch = 8; return key; }
826 if (ch == 9) { key.key = TtyEvent.Key.Tab; key.ch = 9; return key; }
827 //if (ch == 10) { key.key = TtyEvent.Key.Enter; key.ch = 13; return key; }
828 if (ch == 13) { key.key = TtyEvent.Key.Enter; key.ch = 13; return key; }
830 key.key = TtyEvent.Key.Unknown;
832 // escape?
833 if (ch == 27) {
834 ch = ttyReadKeyByte(toEscMSec);
835 if (ch < 0 || ch == 27) { key.key = TtyEvent.Key.Escape; key.ch = 27; return key; }
836 // xterm stupidity
837 if (termType != TermType.rxvt && ch == 'O') {
838 ch = ttyReadKeyByte(toEscMSec);
839 if (ch < 0 || ch == 27) { key.key = TtyEvent.Key.Escape; key.ch = 27; return key; }
840 if (ch >= 'A' && ch <= 'Z') xtermSpecial(cast(char)ch);
841 if (ch >= 'a' && ch <= 'z') { key.shift = true; xtermSpecial(cast(char)(ch-32)); }
842 return key;
844 // csi
845 if (ch == '[') {
846 uint[2] nn;
847 uint nc = 0;
848 bool wasDigit = false;
849 bool firstChar = true;
850 bool linuxCon = false;
851 // parse csi
852 for (;;) {
853 ch = ttyReadKeyByte(toEscMSec);
854 if (firstChar && ch == '<') { parseMouse(); return key; }
855 if (firstChar && ch == 'I') { key.key = TtyEvent.Key.FocusIn; return key; }
856 if (firstChar && ch == 'O') { key.key = TtyEvent.Key.FocusOut; return key; }
857 if (firstChar && ch == '[') { linuxCon = true; firstChar = false; continue; }
858 firstChar = false;
859 if (ch < 0 || ch == 27) { key.key = TtyEvent.Key.Escape; key.ch = 27; return key; }
860 if (ch == ';') {
861 ++nc;
862 if (nc > nn.length) { skipCSI(); return key; }
863 } else if (ch >= '0' && ch <= '9') {
864 if (nc >= nn.length) { skipCSI(); return key; }
865 nn.ptr[nc] = nn.ptr[nc]*10+ch-'0';
866 wasDigit = true;
867 } else {
868 if (wasDigit) ++nc;
869 break;
872 debug(rawtty_show_csi) { import core.stdc.stdio : printf; printf("nc=%u", nc); foreach (uint idx; 0..nc) printf("; n%u=%u", idx, nn.ptr[idx]); printf("; ch=%c\n", ch); }
873 // process specials
874 if (nc == 0) {
875 if (linuxCon) linconSpecial(cast(char)ch);
876 else if (ch >= 'A' && ch <= 'Z') xtermSpecial(cast(char)ch);
877 } else if (nc == 1) {
878 if (ch == '~' && nn.ptr[0] == 200) { key.key = TtyEvent.Key.PasteStart; return key; }
879 if (ch == '~' && nn.ptr[0] == 201) { key.key = TtyEvent.Key.PasteEnd; return key; }
880 switch (ch) {
881 case '~':
882 switch (nn.ptr[0]) {
883 case 23: key.shift = true; key.key = TtyEvent.Key.F1; return key;
884 case 24: key.shift = true; key.key = TtyEvent.Key.F2; return key;
885 case 25: key.shift = true; key.key = TtyEvent.Key.F3; return key;
886 case 26: key.shift = true; key.key = TtyEvent.Key.F4; return key;
887 case 28: key.shift = true; key.key = TtyEvent.Key.F5; return key;
888 case 29: key.shift = true; key.key = TtyEvent.Key.F6; return key;
889 case 31: key.shift = true; key.key = TtyEvent.Key.F7; return key;
890 case 32: key.shift = true; key.key = TtyEvent.Key.F8; return key;
891 case 33: key.shift = true; key.key = TtyEvent.Key.F9; return key;
892 case 34: key.shift = true; key.key = TtyEvent.Key.F10; return key;
893 default:
895 break;
896 case '^': key.ctrl = true; break;
897 case '$': key.shift = true; break;
898 case '@': key.ctrl = true; key.shift = true; break;
899 case 'A': .. case 'Z': xtermMods(nn.ptr[0]); xtermSpecial(cast(char)ch); return key;
900 default: badCSI(); return key;
902 csiSpecial(nn.ptr[0]);
903 } else if (nc == 2 && xtermMods(nn.ptr[1])) {
904 if (nn.ptr[0] == 1 && ch >= 'A' && ch <= 'Z') {
905 xtermSpecial(cast(char)ch);
906 } else if (ch == '~') {
907 csiSpecial(nn.ptr[0]);
909 } else {
910 badCSI();
912 return key;
914 if (ch == 9) {
915 key.key = TtyEvent.Key.Tab;
916 key.alt = true;
917 key.ch = 9;
918 return key;
920 if (ch >= 1 && ch <= 26) {
921 key.key = TtyEvent.Key.ModChar;
922 key.alt = true;
923 key.ctrl = true;
924 key.ch = cast(dchar)(ch+64);
925 if (key.ch == 'H') { key.key = TtyEvent.Key.Backspace; key.ctrl = false; key.ch = 8; }
926 else if (key.ch == 'J') { key.key = TtyEvent.Key.Enter; key.ctrl = false; key.ch = 13; }
927 return key;
929 if (/*(ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '`'*/true) {
930 key.alt = true;
931 key.key = TtyEvent.Key.ModChar;
932 key.shift = (ch >= 'A' && ch <= 'Z'); // ignore capslock
933 if (ch >= 'a' && ch <= 'z') ch -= 32;
934 key.ch = cast(dchar)ch;
935 return key;
937 return key;
940 if (ch == 9) {
941 key.key = TtyEvent.Key.Tab;
942 key.ch = 9;
943 } else if (ch < 32) {
944 // ctrl+letter
945 key.key = TtyEvent.Key.ModChar;
946 key.ctrl = true;
947 key.ch = cast(dchar)(ch+64);
948 if (key.ch == 'H') { key.key = TtyEvent.Key.Backspace; key.ctrl = false; key.ch = 8; }
949 else if (key.ch == 'J') { key.key = TtyEvent.Key.Enter; key.ctrl = false; key.ch = 13; }
950 } else {
951 key.key = TtyEvent.Key.Char;
952 key.ch = cast(dchar)(ch);
953 if (ttyIsFuckedFlag && ch >= 0x80) {
954 Utf8Decoder udc;
955 for (;;) {
956 auto dch = udc.decode(cast(ubyte)ch);
957 if (dch <= dchar.max) break;
958 // want more shit!
959 ch = ttyReadKeyByte(toEscMSec);
960 if (ch < 0) break;
962 if (!udc.invalid) {
963 key.ch = uni2koi(udc.currCodePoint);
965 } else {
966 // xterm does alt+letter with 7th bit set
967 if (!xtermMetaSendsEscape && termType == TermType.xterm && ch >= 0x80 && ch <= 0xff) {
968 ch -= 0x80;
969 if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_') {
970 key.alt = true;
971 key.key = TtyEvent.Key.ModChar;
972 key.shift = (ch >= 'A' && ch <= 'Z'); // ignore capslock
973 if (ch >= 'a' && ch <= 'z') ch -= 32;
974 key.ch = cast(dchar)ch;
975 return key;
980 return key;
984 // ////////////////////////////////////////////////////////////////////////// //
985 // housekeeping
986 private extern(C) void ttyExitRestore () {
987 import core.atomic;
988 if (atomicLoad(inRawMode)) {
989 import core.sys.posix.termios : tcflush, tcsetattr;
990 import core.sys.posix.termios : TCIOFLUSH, TCSAFLUSH;
991 import core.sys.posix.unistd : STDIN_FILENO;
992 //tcflush(STDIN_FILENO, TCIOFLUSH);
993 if (doRestoreOrig) tcsetattr(STDIN_FILENO, TCSAFLUSH, &origMode);
997 shared static this () {
999 import core.stdc.stdlib : atexit;
1000 atexit(&ttyExitRestore);
1003 import core.sys.posix.unistd : isatty, STDIN_FILENO, STDOUT_FILENO;
1004 import core.sys.posix.termios : tcgetattr;
1005 import core.sys.posix.termios : termios;
1006 doRestoreOrig = false;
1007 if (isatty(STDIN_FILENO) && isatty(STDOUT_FILENO)) {
1008 doRestoreOrig = (tcgetattr(STDIN_FILENO, &origMode) == 0);
1014 shared static ~this () {
1015 ttySetNormal();
1019 // ////////////////////////////////////////////////////////////////////////// //
1020 shared static this () {
1021 import core.sys.posix.stdlib : getenv;
1023 ttyIsFuckedFlag = false;
1025 auto lang = getenv("LANG");
1026 if (lang is null) return;
1028 static char tolower (char ch) pure nothrow @safe @nogc { return (ch >= 'A' && ch <= 'Z' ? cast(char)(ch-'A'+'a') : ch); }
1030 while (*lang) {
1031 if (tolower(lang[0]) == 'u' && tolower(lang[1]) == 't' && tolower(lang[2]) == 'f') { ttyIsFuckedFlag = true; return; }
1032 ++lang;