editor: another undo fix
[iv.d.git] / rawtty.d
blob87535ae5376c7e97d0a25f57764b12cc08c79b5f
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, version 3 of the License ONLY.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 // linux tty utilities
18 module iv.rawtty /*is aliced*/;
20 import iv.alice;
21 import core.sys.posix.termios : termios;
22 import iv.strex : strEquCI;
23 import iv.utfutil;
25 public import iv.termrgb;
28 // ////////////////////////////////////////////////////////////////////////// //
29 alias TtyRgb2Color = TtyRGB;
30 alias ttyRgb2Color = ttyRGB;
33 // ////////////////////////////////////////////////////////////////////////// //
34 private __gshared termios origMode;
35 private __gshared bool doRestoreOrig = false;
36 private shared bool inRawMode = false;
38 private class XLock {}
41 // ////////////////////////////////////////////////////////////////////////// //
42 /// TTY mode
43 enum TTYMode {
44 Bad = -1, /// some error occured
45 Normal, /// normal ('cooked') mode
46 Raw /// 'raw' mode
50 __gshared bool xtermMetaSendsEscape = true; /// you should add `XTerm*metaSendsEscape: true` to "~/.Xdefaults"
51 private __gshared bool ttyIsFuckedFlag = false;
54 // ////////////////////////////////////////////////////////////////////////// //
55 /// is TTY fucked with utfuck?
56 @property bool ttyIsUtfucked () nothrow @trusted @nogc { pragma(inline, true); return ttyIsFuckedFlag; }
59 // ////////////////////////////////////////////////////////////////////////// //
60 /// return TTY width
61 @property int ttyWidth () nothrow @trusted @nogc {
62 if (!ttyIsRedirected) {
63 import core.sys.posix.sys.ioctl : ioctl, winsize, TIOCGWINSZ;
64 winsize sz = void;
65 if (ioctl(1, TIOCGWINSZ, &sz) != -1) return sz.ws_col;
67 return 80;
71 /// return TTY height
72 @property int ttyHeight () nothrow @trusted @nogc {
73 if (!ttyIsRedirected) {
74 import core.sys.posix.sys.ioctl : ioctl, winsize, TIOCGWINSZ;
75 winsize sz = void;
76 if (ioctl(1, TIOCGWINSZ, &sz) != -1) return sz.ws_row;
77 return sz.ws_row;
79 return 25;
83 // ////////////////////////////////////////////////////////////////////////// //
84 void ttyRawWrite (const(char)[] str...) nothrow @trusted @nogc {
85 import core.sys.posix.unistd : write;
86 if (str.length) write(1, str.ptr, str.length);
90 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)) {
91 import core.stdc.stdio : snprintf;
92 import core.sys.posix.unistd : write;
93 char[64] buf = void;
94 static if (__traits(isUnsigned, T)) {
95 static if (T.sizeof > 4) {
96 auto len = snprintf(buf.ptr, buf.length, "%llu", n);
97 } else {
98 auto len = snprintf(buf.ptr, buf.length, "%u", cast(uint)n);
100 } else {
101 static if (T.sizeof > 4) {
102 auto len = snprintf(buf.ptr, buf.length, "%lld", n);
103 } else {
104 auto len = snprintf(buf.ptr, buf.length, "%d", cast(int)n);
107 if (len > 0) write(1, buf.ptr, len);
111 void ttyBeep () nothrow @trusted @nogc {
112 import core.sys.posix.unistd : write;
113 enum str = "\x07";
114 write(1, str.ptr, str.length);
118 void ttyEnableBracketedPaste () nothrow @trusted @nogc {
119 import core.sys.posix.unistd : write;
120 enum str = "\x1b[?2004h";
121 write(1, str.ptr, str.length);
125 void ttyDisableBracketedPaste () nothrow @trusted @nogc {
126 import core.sys.posix.unistd : write;
127 enum str = "\x1b[?2004l";
128 write(1, str.ptr, str.length);
132 void ttyEnableFocusReports () nothrow @trusted @nogc {
133 import core.sys.posix.unistd : write;
134 enum str = "\x1b[?1004h";
135 write(1, str.ptr, str.length);
139 void ttyDisableFocusReports () nothrow @trusted @nogc {
140 import core.sys.posix.unistd : write;
141 enum str = "\x1b[?1004l";
142 write(1, str.ptr, str.length);
146 void ttyEnableMouseReports () nothrow @trusted @nogc {
147 import core.sys.posix.unistd : write;
148 enum str = "\x1b[?1000h\x1b[?1006h\x1b[?1002h";
149 write(1, str.ptr, str.length);
153 void ttyDisableMouseReports () nothrow @trusted @nogc {
154 import core.sys.posix.unistd : write;
155 enum str = "\x1b[?1002l\x1b[?1006l\x1b[?1000l";
156 write(1, str.ptr, str.length);
160 // ////////////////////////////////////////////////////////////////////////// //
161 /// get current TTY mode
162 TTYMode ttyGetMode () nothrow @trusted @nogc {
163 import core.atomic;
164 return (atomicLoad(inRawMode) ? TTYMode.Raw : TTYMode.Normal);
168 /// Restore terminal mode we had at program startup
169 void ttyRestoreOrigMode () {
170 import core.atomic;
171 import core.sys.posix.termios : tcflush, tcsetattr;
172 import core.sys.posix.termios : TCIOFLUSH, TCSAFLUSH;
173 import core.sys.posix.unistd : STDIN_FILENO;
174 //tcflush(STDIN_FILENO, TCIOFLUSH);
175 if (doRestoreOrig) tcsetattr(STDIN_FILENO, TCSAFLUSH, &origMode);
176 atomicStore(inRawMode, false);
180 /// returns previous mode or Bad
181 TTYMode ttySetNormal () @trusted @nogc {
182 import core.atomic;
183 if (!doRestoreOrig) return TTYMode.Bad;
184 synchronized(XLock.classinfo) {
185 if (atomicLoad(inRawMode)) {
186 import core.sys.posix.termios : tcflush, tcsetattr;
187 import core.sys.posix.termios : TCIOFLUSH, TCSAFLUSH;
188 import core.sys.posix.unistd : STDIN_FILENO;
189 //tcflush(STDIN_FILENO, TCIOFLUSH);
190 if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &origMode) < 0) return TTYMode.Bad;
191 atomicStore(inRawMode, false);
192 return TTYMode.Raw;
194 return TTYMode.Normal;
199 /// returns previous mode or Bad
200 TTYMode ttySetRaw (bool waitkey=true) @trusted @nogc {
201 import core.atomic;
202 if (ttyIsRedirected || !doRestoreOrig) return TTYMode.Bad;
203 synchronized(XLock.classinfo) {
204 if (!atomicLoad(inRawMode)) {
205 //import core.sys.posix.termios : tcflush, tcsetattr;
206 //import core.sys.posix.termios : TCIOFLUSH, TCSAFLUSH;
207 //import core.sys.posix.termios : BRKINT, CS8, ECHO, ICANON, IEXTEN, INPCK, ISIG, ISTRIP, IXOFF, IGNCR, INLCR, ONLCR, OPOST, VMIN, VTIME;
208 import core.sys.posix.termios;
209 import core.sys.posix.unistd : STDIN_FILENO;
210 import core.stdc.string : memset;
211 enum IUCLC = 512; //0001000
212 //termios raw = origMode; // modify the original mode
213 termios raw = void;
214 memset(&raw, 0, raw.sizeof);
215 //tcflush(STDIN_FILENO, TCIOFLUSH);
216 raw.c_iflag = IGNBRK;
217 // output modes: disable post processing
218 raw.c_oflag = OPOST|ONLCR;
219 // control modes: set 8 bit chars
220 raw.c_cflag = CS8|CLOCAL;
221 // control chars: set return condition: min number of bytes and timer; we want read to return every single byte, without timeout
222 raw.c_cc[VMIN] = (waitkey ? 1 : 0); // wait/poll mode
223 raw.c_cc[VTIME] = 0; // no timer
224 // put terminal in raw mode after flushing
225 if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) < 0) return TTYMode.Bad;
227 import core.sys.posix.unistd : write;
228 // G0 is ASCII, G1 is graphics
229 enum setupStr = "\x1b(B\x1b)0\x0f";
230 write(1, setupStr.ptr, setupStr.length);
232 atomicStore(inRawMode, true);
233 return TTYMode.Normal;
235 return TTYMode.Raw;
240 /// change TTY mode if possible
241 /// returns previous mode or Bad
242 TTYMode ttySetMode (TTYMode mode) @trusted @nogc {
243 // check what we can without locking
244 if (mode == TTYMode.Bad) return TTYMode.Bad;
245 if (ttyIsRedirected) return (mode == TTYMode.Normal ? TTYMode.Normal : TTYMode.Bad);
246 synchronized(XLock.classinfo) return (mode == TTYMode.Normal ? ttySetNormal() : ttySetRaw());
250 // ////////////////////////////////////////////////////////////////////////// //
251 /// set wait/poll mode
252 bool ttySetWaitKey (bool doWait) @trusted @nogc {
253 import core.atomic;
254 if (ttyIsRedirected) return false;
255 synchronized(XLock.classinfo) {
256 if (atomicLoad(inRawMode)) {
257 import core.sys.posix.termios : tcflush, tcgetattr, tcsetattr;
258 import core.sys.posix.termios : TCIOFLUSH, TCSAFLUSH;
259 import core.sys.posix.termios : VMIN;
260 import core.sys.posix.unistd : STDIN_FILENO;
261 termios raw;
262 //tcflush(STDIN_FILENO, TCIOFLUSH);
263 if (tcgetattr(STDIN_FILENO, &raw) != 0) return false; //redirected = false;
264 raw.c_cc[VMIN] = (doWait ? 1 : 0); // wait/poll mode
265 if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) < 0) return false;
266 return true;
269 return false;
273 // ////////////////////////////////////////////////////////////////////////// //
275 * Wait for keypress.
277 * Params:
278 * toMSec = timeout in milliseconds; <0: infinite; 0: don't wait; default is -1
280 * Returns:
281 * true if key was pressed, false if no key was pressed in the given time
283 bool ttyWaitKey (int toMSec=-1) @trusted @nogc {
284 import core.atomic;
285 if (!ttyIsRedirected && atomicLoad(inRawMode)) {
286 import core.sys.posix.sys.select : fd_set, select, timeval, FD_ISSET, FD_SET, FD_ZERO;
287 import core.sys.posix.unistd : STDIN_FILENO;
288 timeval tv;
289 fd_set fds;
290 FD_ZERO(&fds);
291 FD_SET(STDIN_FILENO, &fds); //STDIN_FILENO is 0
292 if (toMSec <= 0) {
293 tv.tv_sec = 0;
294 tv.tv_usec = 0;
295 } else {
296 tv.tv_sec = cast(int)(toMSec/1000);
297 tv.tv_usec = (toMSec%1000)*1000;
299 select(STDIN_FILENO+1, &fds, null, null, (toMSec < 0 ? null : &tv));
300 return FD_ISSET(STDIN_FILENO, &fds);
302 return false;
307 * Check if key was pressed. Don't block.
309 * Returns:
310 * true if key was pressed, false if no key was pressed
312 bool ttyIsKeyHit () @trusted @nogc { return ttyWaitKey(0); }
316 * Read one byte from stdin.
318 * Params:
319 * toMSec = timeout in milliseconds; <0: infinite; 0: don't wait; default is -1
321 * Returns:
322 * read byte or -1 on error/timeout
324 int ttyReadKeyByte (int toMSec=-1) @trusted @nogc {
325 import core.atomic;
326 if (!ttyIsRedirected && atomicLoad(inRawMode)) {
327 import core.sys.posix.unistd : read, STDIN_FILENO;
328 ubyte res;
329 if (toMSec >= 0) {
330 synchronized(XLock.classinfo) if (ttyWaitKey(toMSec) && read(STDIN_FILENO, &res, 1) == 1) return res;
331 } else {
332 if (read(STDIN_FILENO, &res, 1) == 1) {
333 //{ import core.stdc.stdio; if (res > 32 && res != 127) printf("[%c]\n", res); else printf("{%d}\n", res); }
334 return res;
338 return -1;
342 // ////////////////////////////////////////////////////////////////////////// //
343 /// pressed key info
344 public align(1) struct TtyEvent {
345 align(1): // make it tightly packed
347 enum Key : ubyte {
348 None, ///
349 Error, /// error reading key
350 Unknown, /// can't interpret escape code
352 Char, ///
354 // for bracketed paste mode
355 PasteStart,
356 PasteEnd,
358 ModChar, /// char with some modifier
360 Up, ///
361 Down, ///
362 Left, ///
363 Right, ///
364 Insert, ///
365 Delete, ///
366 PageUp, ///
367 PageDown, ///
368 Home, ///
369 End, ///
371 Escape, ///
372 Backspace, ///
373 Tab, ///
374 Enter, ///
376 Pad5, /// xterm can return this
378 F1, ///
379 F2, ///
380 F3, ///
381 F4, ///
382 F5, ///
383 F6, ///
384 F7, ///
385 F8, ///
386 F9, ///
387 F10, ///
388 F11, ///
389 F12, ///
391 //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,
392 //N0, N1, N2, N3, N4, N5, N6, N7, N8, N9,
394 MLeftDown, ///
395 MLeftUp, ///
396 MLeftMotion, ///
397 MMiddleDown, ///
398 MMiddleUp, ///
399 MMiddleMotion, ///
400 MRightDown, ///
401 MRightUp, ///
402 MRightMotion, ///
404 MWheelUp, ///
405 MWheelDown, ///
407 MMotion, /// mouse motion without buttons, not read from tty by now, but can be useful for other backends
409 // synthesized events, used in tui
410 MLeftClick, ///
411 MMiddleClick, ///
412 MRightClick, ///
414 MLeftDouble, ///
415 MMiddleDouble, ///
416 MRightDouble, ///
418 FocusIn, ///
419 FocusOut, ///
423 enum ModFlag : ubyte {
424 Ctrl = 1<<0, ///
425 Alt = 1<<1, ///
426 Shift = 1<<2, ///
430 enum MButton : int {
431 None = 0, ///
432 Left = 1, ///
433 Middle = 2, ///
434 Right = 3, ///
435 WheelUp = 4, ///
436 WheelDown = 5, ///
437 First = Left, ///
440 Key key; /// key type/sym
441 ubyte mods; /// set of ModFlag
442 dchar ch = 0; /// can be 0 for special key
443 short x, y; /// for mouse reports
444 //void* udata; /// arbitrary user data
446 @property const pure nothrow @safe @nogc {
448 MButton button () { pragma(inline, true); return
449 key == Key.MLeftDown || key == Key.MLeftUp || key == Key.MLeftMotion || key == Key.MLeftClick || key == Key.MLeftDouble ? MButton.Left :
450 key == Key.MRightDown || key == Key.MRightUp || key == Key.MRightMotion || key == Key.MRightClick || key == Key.MRightDouble ? MButton.Right :
451 key == Key.MMiddleDown || key == Key.MMiddleUp || key == Key.MMiddleMotion || key == Key.MMiddleClick || key == Key.MMiddleDouble ? MButton.Middle :
452 key == Key.MWheelUp ? MButton.WheelUp :
453 key == Key.MWheelDown ? MButton.WheelDown :
454 MButton.None;
456 bool mouse () { pragma(inline, true); return (key >= Key.MLeftDown && key <= Key.MRightDouble); } ///
457 bool mpress () { pragma(inline, true); return (key == Key.MLeftDown || key == Key.MRightDown || key == Key.MMiddleDown); } ///
458 bool mrelease () { pragma(inline, true); return (key == Key.MLeftUp || key == Key.MRightUp || key == Key.MMiddleUp); } ///
459 bool mclick () { pragma(inline, true); return (key == Key.MLeftClick || key == Key.MRightClick || key == Key.MMiddleClick); } ///
460 bool mdouble () { pragma(inline, true); return (key == Key.MLeftDouble || key == Key.MRightDouble || key == Key.MMiddleDouble); } ///
461 bool mmotion () { pragma(inline, true); return (key == Key.MLeftMotion || key == Key.MRightMotion || key == Key.MMiddleMotion || key == Key.MMotion); } ///
462 bool mwheel () { pragma(inline, true); return (key == Key.MWheelUp || key == Key.MWheelDown); } ///
463 bool focusin () { pragma(inline, true); return (key == Key.FocusIn); } ///
464 bool focusout () { pragma(inline, true); return (key == Key.FocusOut); } ///
465 bool ctrl () { pragma(inline, true); return ((mods&ModFlag.Ctrl) != 0); } ///
466 bool alt () { pragma(inline, true); return ((mods&ModFlag.Alt) != 0); } ///
467 bool shift () { pragma(inline, true); return ((mods&ModFlag.Shift) != 0); } ///
470 @property pure nothrow @safe @nogc {
471 void ctrl (bool v) { pragma(inline, true); if (v) mods |= ModFlag.Ctrl; else mods &= ~(ModFlag.Ctrl); } ///
472 void alt (bool v) { pragma(inline, true); if (v) mods |= ModFlag.Alt; else mods &= ~(ModFlag.Alt); } ///
473 void shift (bool v) { pragma(inline, true); if (v) mods |= ModFlag.Shift; else mods &= ~(ModFlag.Shift); } ///
476 this (const(char)[] s) pure nothrow @safe @nogc {
477 if (TtyEvent.parse(this, s).length != 0) {
478 key = Key.Error;
479 mods = 0;
480 ch = 0;
484 bool opEquals (in TtyEvent k) const pure nothrow @safe @nogc {
485 pragma(inline, true);
486 return
487 (key == k.key ?
488 (key == Key.Char ? (ch == k.ch) :
489 key == Key.ModChar ? (mods == k.mods && ch == k.ch) :
490 //key >= Key.MLeftDown && key <= MWheelDown ? true :
491 key > Key.ModChar ? (mods == k.mods) :
492 true
493 ) : false
497 bool opEquals (const(char)[] s) const pure nothrow @safe @nogc {
498 TtyEvent k;
499 if (TtyEvent.parse(k, s).length != 0) return false;
500 return (k == this);
504 string toString () const nothrow {
505 char[128] buf = void;
506 return toCharBuf(buf[]).idup;
510 char[] toCharBuf (char[] dest) const nothrow @trusted @nogc {
511 static immutable string hexD = "0123456789abcdef";
512 int dpos = 0;
513 void put (const(char)[] s...) nothrow @nogc {
514 foreach (char ch; s) {
515 if (dpos >= dest.length) break;
516 dest.ptr[dpos++] = ch;
519 void putMods () nothrow @nogc {
520 if (ctrl) put("C-");
521 if (alt) put("M-");
522 if (shift) put("S-");
524 if (key == Key.ModChar) putMods();
525 if (key == Key.Char || key == Key.ModChar) {
526 if (ch < ' ' || ch == 127) {
527 put("x");
528 put(hexD.ptr[(ch>>4)&0x0f]);
529 put(hexD.ptr[ch&0x0f]);
530 } else if (ch == ' ') {
531 put("space");
532 } else if (ch < 256) {
533 put(cast(char)ch);
534 } else if (ch <= 0xffff) {
535 put("u");
536 put(hexD.ptr[(ch>>12)&0x0f]);
537 put(hexD.ptr[(ch>>8)&0x0f]);
538 put(hexD.ptr[(ch>>4)&0x0f]);
539 put(hexD.ptr[ch&0x0f]);
540 } else {
541 put("error");
543 return dest[0..dpos];
545 if (key == Key.None) { put("none"); return dest[0..dpos]; }
546 if (key == Key.Error) { put("error"); return dest[0..dpos]; }
547 if (key == Key.Unknown) { put("unknown"); return dest[0..dpos]; }
548 foreach (string kn; __traits(allMembers, TtyEvent.Key)) {
549 if (__traits(getMember, TtyEvent.Key, kn) == key) {
550 putMods();
551 put(kn);
552 return dest[0..dpos];
555 put("error");
556 return dest[0..dpos];
559 /** parse key name. get first word, return rest of the string (with trailing spaces removed)
561 * "C-<home>" (emacs-like syntax is recognized)
563 * "C-M-x"
565 * mods: C(trl), M(eta:alt), S(hift)
567 * `key` will be `TtyEvent.Key.Error` on error, `TtyEvent.Key.None` on empty string
569 static T parse(T) (out TtyEvent key, T s) pure nothrow @trusted @nogc if (is(T : const(char)[])) {
570 static if (is(T == typeof(null))) {
571 return null;
572 } else {
573 while (s.length && s.ptr[0] <= ' ') s = s[1..$];
574 if (s.length == 0) return s; // no more
575 // get space-delimited word
576 int pos = 1; // 0 is always non-space here
577 while (pos < s.length && s.ptr[pos] > ' ') { if (++pos >= 1024) return s; }
578 auto olds = s; // return this in case of error
579 const(char)[] str = s[0..pos]; // string to parse
580 // `s` will be our result; remove leading spaces for convenience
581 while (pos < s.length && s.ptr[pos] <= ' ') ++pos;
582 s = s[pos..$];
583 // parse word
584 while (str.length > 0) {
585 if (str.length >= 2 && str.ptr[1] == '-') {
586 // modifier
587 switch (str.ptr[0]) {
588 case 'C': case 'c': key.ctrl = true; break;
589 case 'M': case 'm': key.alt = true; break;
590 case 'S': case 's': key.shift = true; break;
591 default: goto error; // unknown modifier
593 str = str[2..$];
594 } else {
595 // key
596 if (str.length > 1 && str.ptr[0] == '^') {
597 // ^A means C-A
598 key.ctrl = true;
599 str = str[1..$];
600 } else if (str.length > 2 && str.ptr[0] == '<' && str[$-1] == '>') {
601 str = str[1..$-1];
603 if (str.length == 0) goto error; // just in case
604 if (str.strEquCI("space")) str = " ";
605 if (str.length == 1) {
606 // single char
607 key.ch = str.ptr[0];
608 if (key.ctrl || key.alt) {
609 key.key = TtyEvent.Key.ModChar;
610 if (key.ch >= 'a' && key.ch <= 'z') key.ch -= 32; // toupper
611 } else {
612 key.key = TtyEvent.Key.Char;
613 if (key.shift) {
614 if (key.ch >= 'a' && key.ch <= 'z') key.ch -= 32; // toupper
615 else switch (key.ch) {
616 case '`': key.ch = '~'; break;
617 case '1': key.ch = '!'; break;
618 case '2': key.ch = '@'; break;
619 case '3': key.ch = '#'; break;
620 case '4': key.ch = '$'; break;
621 case '5': key.ch = '%'; break;
622 case '6': key.ch = '^'; break;
623 case '7': key.ch = '&'; break;
624 case '8': key.ch = '*'; break;
625 case '9': key.ch = '('; break;
626 case '0': key.ch = ')'; break;
627 case '-': 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 default:
639 key.shift = false;
642 } else {
643 // key name
644 if (str.strEquCI("return")) str = "enter";
645 if (str.strEquCI("esc")) str = "escape";
646 if (str.strEquCI("bs")) str = "backspace";
647 if (str.strEquCI("PasteStart") || str.strEquCI("Paste-Start")) {
648 key.key = TtyEvent.Key.PasteStart;
649 key.mods = 0;
650 key.ch = 0;
651 } else if (str.strEquCI("PasteEnd") || str.strEquCI("Paste-End")) {
652 key.key = TtyEvent.Key.PasteEnd;
653 key.mods = 0;
654 key.ch = 0;
655 } else {
656 bool found = false;
657 foreach (string kn; __traits(allMembers, TtyEvent.Key)) {
658 if (!found && str.strEquCI(kn)) {
659 found = true;
660 key.key = __traits(getMember, TtyEvent.Key, kn);
661 break;
664 if (!found || key.key < TtyEvent.Key.Up) goto error;
666 // just in case
667 if (key.key == TtyEvent.Key.Enter) key.ch = 13;
668 else if (key.key == TtyEvent.Key.Tab) key.ch = 9;
669 else if (key.key == TtyEvent.Key.Escape) key.ch = 27;
670 else if (key.key == TtyEvent.Key.Backspace) key.ch = 8;
672 return s;
675 error:
676 key = TtyEvent.init;
677 key.key = TtyEvent.Key.Error;
678 return olds;
685 * Read key from stdin.
687 * WARNING! no utf-8 support yet!
689 * Params:
690 * toMSec = timeout in milliseconds; <0: infinite; 0: don't wait; default is -1
691 * toEscMSec = timeout in milliseconds for escape sequences
693 * Returns:
694 * null on error or keyname
696 TtyEvent ttyReadKey (int toMSec=-1, int toEscMSec=-1/*300*/) @trusted @nogc {
697 TtyEvent key;
699 void skipCSI () @nogc {
700 key.key = TtyEvent.Key.Unknown;
701 for (;;) {
702 auto ch = ttyReadKeyByte(toEscMSec);
703 if (ch < 0 || ch == 27) { key.key = TtyEvent.Key.Escape; key.ch = 27; break; }
704 if (ch != ';' && (ch < '0' || ch > '9')) break;
708 void badCSI () @nogc {
709 key = key.init;
710 key.key = TtyEvent.Key.Unknown;
713 bool xtermMods (uint mci) @nogc {
714 switch (mci) {
715 case 2: key.shift = true; return true;
716 case 3: key.alt = true; return true;
717 case 4: key.alt = true; key.shift = true; return true;
718 case 5: key.ctrl = true; return true;
719 case 6: key.ctrl = true; key.shift = true; return true;
720 case 7: key.alt = true; key.ctrl = true; return true;
721 case 8: key.alt = true; key.ctrl = true; key.shift = true; return true;
722 default:
724 return false;
727 void xtermSpecial (char ch) @nogc {
728 switch (ch) {
729 case 'A': key.key = TtyEvent.Key.Up; break;
730 case 'B': key.key = TtyEvent.Key.Down; break;
731 case 'C': key.key = TtyEvent.Key.Right; break;
732 case 'D': key.key = TtyEvent.Key.Left; break;
733 case 'E': key.key = TtyEvent.Key.Pad5; break;
734 case 'H': key.key = TtyEvent.Key.Home; break;
735 case 'F': key.key = TtyEvent.Key.End; break;
736 case 'P': key.key = TtyEvent.Key.F1; break;
737 case 'Q': key.key = TtyEvent.Key.F2; break;
738 case 'R': key.key = TtyEvent.Key.F3; break;
739 case 'S': key.key = TtyEvent.Key.F4; break;
740 case 'Z': key.key = TtyEvent.Key.Tab; key.ch = 9; if (!key.shift && !key.alt && !key.ctrl) key.shift = true; break;
741 default: badCSI(); break;
745 void linconSpecial (char ch) @nogc {
746 switch (ch) {
747 case 'A': key.key = TtyEvent.Key.F1; break;
748 case 'B': key.key = TtyEvent.Key.F2; break;
749 case 'C': key.key = TtyEvent.Key.F3; break;
750 case 'D': key.key = TtyEvent.Key.F4; break;
751 default: badCSI(); break;
755 void csiSpecial (uint n) @nogc {
756 switch (n) {
757 case 1: key.key = TtyEvent.Key.Home; return; // xterm
758 case 2: key.key = TtyEvent.Key.Insert; return;
759 case 3: key.key = TtyEvent.Key.Delete; return;
760 case 4: key.key = TtyEvent.Key.End; return;
761 case 5: key.key = TtyEvent.Key.PageUp; return;
762 case 6: key.key = TtyEvent.Key.PageDown; return;
763 case 7: key.key = TtyEvent.Key.Home; return; // rxvt
764 case 8: key.key = TtyEvent.Key.End; return;
765 case 1+10: key.key = TtyEvent.Key.F1; return;
766 case 2+10: key.key = TtyEvent.Key.F2; return;
767 case 3+10: key.key = TtyEvent.Key.F3; return;
768 case 4+10: key.key = TtyEvent.Key.F4; return;
769 case 5+10: key.key = TtyEvent.Key.F5; return;
770 case 6+11: key.key = TtyEvent.Key.F6; return;
771 case 7+11: key.key = TtyEvent.Key.F7; return;
772 case 8+11: key.key = TtyEvent.Key.F8; return;
773 case 9+11: key.key = TtyEvent.Key.F9; return;
774 case 10+11: key.key = TtyEvent.Key.F10; return;
775 case 11+12: key.key = TtyEvent.Key.F11; return;
776 case 12+12: key.key = TtyEvent.Key.F12; return;
777 default: badCSI(); break;
781 // {\e}[<0;58;32M (button;x;y;[Mm])
782 void parseMouse () @nogc {
783 uint[3] nn;
784 uint nc = 0;
785 bool press = false;
786 for (;;) {
787 auto ch = ttyReadKeyByte(toEscMSec);
788 if (ch < 0 || ch == 27) { key.key = TtyEvent.Key.Escape; key.ch = 27; return; }
789 if (ch == ';') {
790 ++nc;
791 } else if (ch >= '0' && ch <= '9') {
792 if (nc < nn.length) nn.ptr[nc] = nn.ptr[nc]*10+ch-'0';
793 } else {
794 if (ch == 'M') press = true;
795 else if (ch == 'm') press = false;
796 else { key.key = TtyEvent.Key.Unknown; return; }
797 break;
800 if (nn[1] > 0) --nn[1];
801 if (nn[2] > 0) --nn[2];
802 if (nn[1] < 0) nn[1] = 1;
803 if (nn[1] > short.max) nn[1] = short.max;
804 if (nn[2] < 0) nn[2] = 1;
805 if (nn[2] > short.max) nn[2] = short.max;
806 switch (nn[0]) {
807 case 0: key.key = (press ? TtyEvent.Key.MLeftDown : TtyEvent.Key.MLeftUp); break;
808 case 1: key.key = (press ? TtyEvent.Key.MMiddleDown : TtyEvent.Key.MMiddleUp); break;
809 case 2: key.key = (press ? TtyEvent.Key.MRightDown : TtyEvent.Key.MRightUp); break;
810 case 32: if (!press) { key.key = TtyEvent.Key.Unknown; return; } key.key = TtyEvent.Key.MLeftMotion; break;
811 case 33: if (!press) { key.key = TtyEvent.Key.Unknown; return; } key.key = TtyEvent.Key.MMiddleMotion; break;
812 case 34: if (!press) { key.key = TtyEvent.Key.Unknown; return; } key.key = TtyEvent.Key.MRightMotion; break;
813 case 64: if (!press) { key.key = TtyEvent.Key.Unknown; return; } key.key = TtyEvent.Key.MWheelUp; break;
814 case 65: if (!press) { key.key = TtyEvent.Key.Unknown; return; } key.key = TtyEvent.Key.MWheelDown; break;
815 default: key.key = TtyEvent.Key.Unknown; return;
817 key.x = cast(short)nn[1];
818 key.y = cast(short)nn[2];
821 int ch = ttyReadKeyByte(toMSec);
822 if (ch < 0) { key.key = TtyEvent.Key.Error; return key; } // error
823 if (ch == 0) { key.key = TtyEvent.Key.ModChar; key.ctrl = true; key.ch = ' '; return key; }
824 if (ch == 8 || ch == 127) { key.key = TtyEvent.Key.Backspace; key.ch = 8; return key; }
825 if (ch == 9) { key.key = TtyEvent.Key.Tab; key.ch = 9; return key; }
826 //if (ch == 10) { key.key = TtyEvent.Key.Enter; key.ch = 13; return key; }
827 if (ch == 13) { key.key = TtyEvent.Key.Enter; key.ch = 13; return key; }
829 key.key = TtyEvent.Key.Unknown;
831 // escape?
832 if (ch == 27) {
833 ch = ttyReadKeyByte(toEscMSec);
834 if (ch < 0 || ch == 27) { key.key = TtyEvent.Key.Escape; key.ch = 27; return key; }
835 // xterm stupidity
836 if (termType != TermType.rxvt && ch == 'O') {
837 ch = ttyReadKeyByte(toEscMSec);
838 if (ch < 0 || ch == 27) { key.key = TtyEvent.Key.Escape; key.ch = 27; return key; }
839 if (ch >= 'A' && ch <= 'Z') xtermSpecial(cast(char)ch);
840 if (ch >= 'a' && ch <= 'z') { key.shift = true; xtermSpecial(cast(char)(ch-32)); }
841 return key;
843 // csi
844 if (ch == '[') {
845 uint[2] nn;
846 uint nc = 0;
847 bool wasDigit = false;
848 bool firstChar = true;
849 bool linuxCon = false;
850 // parse csi
851 for (;;) {
852 ch = ttyReadKeyByte(toEscMSec);
853 if (firstChar && ch == '<') { parseMouse(); return key; }
854 if (firstChar && ch == 'I') { key.key = TtyEvent.Key.FocusIn; return key; }
855 if (firstChar && ch == 'O') { key.key = TtyEvent.Key.FocusOut; return key; }
856 if (firstChar && ch == '[') { linuxCon = true; firstChar = false; continue; }
857 firstChar = false;
858 if (ch < 0 || ch == 27) { key.key = TtyEvent.Key.Escape; key.ch = 27; return key; }
859 if (ch == ';') {
860 ++nc;
861 if (nc > nn.length) { skipCSI(); return key; }
862 } else if (ch >= '0' && ch <= '9') {
863 if (nc >= nn.length) { skipCSI(); return key; }
864 nn.ptr[nc] = nn.ptr[nc]*10+ch-'0';
865 wasDigit = true;
866 } else {
867 if (wasDigit) ++nc;
868 break;
871 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); }
872 // process specials
873 if (nc == 0) {
874 if (linuxCon) linconSpecial(cast(char)ch);
875 else if (ch >= 'A' && ch <= 'Z') xtermSpecial(cast(char)ch);
876 } else if (nc == 1) {
877 if (ch == '~' && nn.ptr[0] == 200) { key.key = TtyEvent.Key.PasteStart; return key; }
878 if (ch == '~' && nn.ptr[0] == 201) { key.key = TtyEvent.Key.PasteEnd; return key; }
879 switch (ch) {
880 case '~':
881 switch (nn.ptr[0]) {
882 case 23: key.shift = true; key.key = TtyEvent.Key.F1; return key;
883 case 24: key.shift = true; key.key = TtyEvent.Key.F2; return key;
884 case 25: key.shift = true; key.key = TtyEvent.Key.F3; return key;
885 case 26: key.shift = true; key.key = TtyEvent.Key.F4; return key;
886 case 28: key.shift = true; key.key = TtyEvent.Key.F5; return key;
887 case 29: key.shift = true; key.key = TtyEvent.Key.F6; return key;
888 case 31: key.shift = true; key.key = TtyEvent.Key.F7; return key;
889 case 32: key.shift = true; key.key = TtyEvent.Key.F8; return key;
890 case 33: key.shift = true; key.key = TtyEvent.Key.F9; return key;
891 case 34: key.shift = true; key.key = TtyEvent.Key.F10; return key;
892 default:
894 break;
895 case '^': key.ctrl = true; break;
896 case '$': key.shift = true; break;
897 case '@': key.ctrl = true; key.shift = true; break;
898 case 'A': .. case 'Z': xtermMods(nn.ptr[0]); xtermSpecial(cast(char)ch); return key;
899 default: badCSI(); return key;
901 csiSpecial(nn.ptr[0]);
902 } else if (nc == 2 && xtermMods(nn.ptr[1])) {
903 if (nn.ptr[0] == 1 && ch >= 'A' && ch <= 'Z') {
904 xtermSpecial(cast(char)ch);
905 } else if (ch == '~') {
906 csiSpecial(nn.ptr[0]);
908 } else {
909 badCSI();
911 return key;
913 if (ch == 9) {
914 key.key = TtyEvent.Key.Tab;
915 key.alt = true;
916 key.ch = 9;
917 return key;
919 if (ch >= 1 && ch <= 26) {
920 key.key = TtyEvent.Key.ModChar;
921 key.alt = true;
922 key.ctrl = true;
923 key.ch = cast(dchar)(ch+64);
924 if (key.ch == 'H') { key.key = TtyEvent.Key.Backspace; key.ctrl = false; key.ch = 8; }
925 else if (key.ch == 'J') { key.key = TtyEvent.Key.Enter; key.ctrl = false; key.ch = 13; }
926 return key;
928 if (/*(ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '`'*/true) {
929 key.alt = true;
930 key.key = TtyEvent.Key.ModChar;
931 key.shift = (ch >= 'A' && ch <= 'Z'); // ignore capslock
932 if (ch >= 'a' && ch <= 'z') ch -= 32;
933 key.ch = cast(dchar)ch;
934 return key;
936 return key;
939 if (ch == 9) {
940 key.key = TtyEvent.Key.Tab;
941 key.ch = 9;
942 } else if (ch < 32) {
943 // ctrl+letter
944 key.key = TtyEvent.Key.ModChar;
945 key.ctrl = true;
946 key.ch = cast(dchar)(ch+64);
947 if (key.ch == 'H') { key.key = TtyEvent.Key.Backspace; key.ctrl = false; key.ch = 8; }
948 else if (key.ch == 'J') { key.key = TtyEvent.Key.Enter; key.ctrl = false; key.ch = 13; }
949 } else {
950 key.key = TtyEvent.Key.Char;
951 key.ch = cast(dchar)(ch);
952 if (ttyIsFuckedFlag && ch >= 0x80) {
953 Utf8Decoder udc;
954 for (;;) {
955 auto dch = udc.decode(cast(ubyte)ch);
956 if (dch <= dchar.max) break;
957 // want more shit!
958 ch = ttyReadKeyByte(toEscMSec);
959 if (ch < 0) break;
961 if (!udc.invalid) {
962 key.ch = uni2koi(udc.currCodePoint);
964 } else {
965 // xterm does alt+letter with 7th bit set
966 if (!xtermMetaSendsEscape && termType == TermType.xterm && ch >= 0x80 && ch <= 0xff) {
967 ch -= 0x80;
968 if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_') {
969 key.alt = true;
970 key.key = TtyEvent.Key.ModChar;
971 key.shift = (ch >= 'A' && ch <= 'Z'); // ignore capslock
972 if (ch >= 'a' && ch <= 'z') ch -= 32;
973 key.ch = cast(dchar)ch;
974 return key;
979 return key;
983 // ////////////////////////////////////////////////////////////////////////// //
984 // housekeeping
985 private extern(C) void ttyExitRestore () {
986 import core.atomic;
987 if (atomicLoad(inRawMode)) {
988 import core.sys.posix.termios : tcflush, tcsetattr;
989 import core.sys.posix.termios : TCIOFLUSH, TCSAFLUSH;
990 import core.sys.posix.unistd : STDIN_FILENO;
991 //tcflush(STDIN_FILENO, TCIOFLUSH);
992 if (doRestoreOrig) tcsetattr(STDIN_FILENO, TCSAFLUSH, &origMode);
996 shared static this () {
998 import core.stdc.stdlib : atexit;
999 atexit(&ttyExitRestore);
1002 import core.sys.posix.unistd : isatty, STDIN_FILENO, STDOUT_FILENO;
1003 import core.sys.posix.termios : tcgetattr;
1004 import core.sys.posix.termios : termios;
1005 doRestoreOrig = false;
1006 if (isatty(STDIN_FILENO) && isatty(STDOUT_FILENO)) {
1007 doRestoreOrig = (tcgetattr(STDIN_FILENO, &origMode) == 0);
1013 shared static ~this () {
1014 ttySetNormal();
1018 // ////////////////////////////////////////////////////////////////////////// //
1019 shared static this () {
1020 import core.sys.posix.stdlib : getenv;
1022 ttyIsFuckedFlag = false;
1024 auto lang = getenv("LANG");
1025 if (lang is null) return;
1027 static char tolower (char ch) pure nothrow @safe @nogc { return (ch >= 'A' && ch <= 'Z' ? cast(char)(ch-'A'+'a') : ch); }
1029 while (*lang) {
1030 if (tolower(lang[0]) == 'u' && tolower(lang[1]) == 't' && tolower(lang[2]) == 'f') { ttyIsFuckedFlag = true; return; }
1031 ++lang;