egra: checkbox fixes
[iv.d.git] / tuing / tty.d
blob37c64655c8d7569583f0a8af0ca5c8d4762755c7
1 /* Invisible Vector Library
2 * simple FlexBox-based TUI engine
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, version 3 of the License ONLY.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 // virtual console with doublebuffering, to awoid alot of overdraw
17 module iv.tuing.tty /*is aliced*/;
18 private:
20 import iv.alice;
21 public import iv.rawtty;
22 import iv.strex;
23 import iv.utfutil;
26 // ////////////////////////////////////////////////////////////////////////// //
27 public __gshared int TtyDefaultEscWait = 50; // -1: forever
28 private __gshared int ttywIntr, ttyhIntr; // DO NOT CHANGE!
29 __gshared bool weAreFucked = false; // utfucked?
30 __gshared string ttyzTitleSuffix;
32 public @property int ttyw () nothrow @trusted @nogc { pragma(inline, true); return ttywIntr; }
33 public @property int ttyh () nothrow @trusted @nogc { pragma(inline, true); return ttyhIntr; }
35 public @property string ttyTitleSuffix () nothrow @trusted @nogc { return ttyzTitleSuffix; }
36 public @property void ttyTitleSuffix(T : const(char)[]) (T s) nothrow {
37 static if (is(T == typeof(null))) {
38 ttyzTitleSuffix = null;
39 } else static if (is(T == string)) {
40 ttyzTitleSuffix = s;
41 } else {
42 ttyzTitleSuffix = s.idup;
47 // ////////////////////////////////////////////////////////////////////////// //
48 public enum XtColorFB(ubyte fg, ubyte bg) = cast(uint)((fg<<8)|bg);
51 // ////////////////////////////////////////////////////////////////////////// //
52 // global scissors
53 public align(1) struct XtScissor {
54 align(1) nothrow @trusted @nogc:
55 private:
56 ushort mX0, mY0, mW, mH;
58 public:
59 this (int ax0, int ay0, int aw, int ah) {
60 if (ay0 >= ttyhIntr || ax0 >= ttywIntr || aw < 1 || ah < 1) return;
61 if (ax0 < 0) {
62 if (ax0 <= -aw) return;
63 aw += ax0;
64 ax0 = 0;
66 if (ay0 < 0) {
67 if (ay0 <= -ah) return;
68 ah += ay0;
69 ay0 = 0;
71 if (aw > ttywIntr) aw = ttywIntr;
72 if (ah > ttyhIntr) ah = ttyhIntr;
73 if (ttywIntr-ax0 > aw) aw = ttywIntr-ax0;
74 if (ttyhIntr-ay0 > ah) ah = ttyhIntr-ay0;
75 if (aw > 0 && ah > 0) {
76 mX0 = cast(ushort)ax0;
77 mY0 = cast(ushort)ay0;
78 mW = cast(ushort)aw;
79 mH = cast(ushort)ah;
83 static XtScissor fullscreen () { return XtScissor(0, 0, ttywIntr, ttyhIntr); }
85 const @safe:
86 // crop this scissor with another scissor
87 XtScissor crop (int x, int y, int w, int h) { return crop(XtScissor(x, y, w, h)); }
88 pure:
89 XtScissor crop (in XtScissor s) {
90 import std.algorithm : max, min;
91 XtScissor res = void;
92 res.mX0 = this.mX0;
93 res.mY0 = this.mY0;
94 res.mW = this.mW;
95 res.mH = this.mH;
96 if (res.visible && s.visible) {
97 int rx0 = max(mX0, s.mX0);
98 int ry0 = max(mY0, s.mY0);
99 int rx1 = min(x1, s.x1);
100 int ry1 = min(y1, s.y1);
101 if (rx1 < rx0 || ry1 < ry0) {
102 res.mW = res.mH = 0;
103 } else {
104 res.mX0 = cast(ushort)rx0;
105 res.mY0 = cast(ushort)ry0;
106 res.mW = cast(ushort)(rx1-rx0+1);
107 res.mH = cast(ushort)(ry1-ry0+1);
110 return res;
113 @property:
114 bool visible () { pragma(inline, true); return (mW > 0 && mH > 0); }
115 bool empty () { pragma(inline, true); return (mW == 0 && mH == 0); }
116 int x0 () { pragma(inline, true); return mX0; }
117 int y0 () { pragma(inline, true); return mY0; }
118 int x1 () { pragma(inline, true); return mX0+mW-1; } // may be negative for empty scissor
119 int y1 () { pragma(inline, true); return mY0+mH-1; } // may be negative for empty scissor
120 int width () { pragma(inline, true); return mW; }
121 int height () { pragma(inline, true); return mH; }
124 __gshared XtScissor ttyzScissor;
127 public @property XtScissor ttyScissor () nothrow @trusted @nogc { pragma(inline, true); return ttyzScissor; }
128 public @property void ttyScissor (in XtScissor sc) nothrow @trusted @nogc { pragma(inline, true); ttyzScissor = sc; }
131 // ////////////////////////////////////////////////////////////////////////// //
132 enum SIGWINCH = 28;
134 __gshared bool winSizeChanged = false;
135 __gshared bool winChSet = false;
137 extern(C) void sigwinchHandler (int sig) {
138 winSizeChanged = true;
141 void setupWinch () {
142 import core.sys.posix.signal;
143 if (winChSet) return;
144 winChSet = true;
145 sigaction_t sa;
146 sigemptyset(&sa.sa_mask);
147 sa.sa_flags = 0;
148 sa.sa_handler = &sigwinchHandler;
149 if (sigaction(SIGWINCH, &sa, null) == -1) return;
153 // ////////////////////////////////////////////////////////////////////////// //
154 align(1) struct Glyph {
155 align(1):
156 enum Flag : ubyte {
157 G1 = 0b1000_0000u, // only this will be checked in refresh
158 Mask = 0b1000_0000u, // refresh compare mask
159 GraphMask = 0b0000_1111u,
160 GraphUp = 0b0000_0001u,
161 GraphDown = 0b0000_0010u,
162 GraphLeft = 0b0000_0100u,
163 GraphRight = 0b0000_1000u,
165 ubyte fg = 7; // foreground color
166 ubyte bg = 0; // background color
167 char ch = 0; // char
168 ubyte flags = 0; // see Flag enum
169 // 0: not a graphic char
170 @property char g1char () const pure nothrow @trusted @nogc {
171 static immutable char[16] transTbl = [
172 0x20, // ....
173 0x78, // ...U
174 0x78, // ..D.
175 0x78, // ..DU
176 0x71, // .L..
177 0x6A, // .L.U
178 0x6B, // .LD.
179 0x75, // .LDU
180 0x71, // R...
181 0x6D, // R..U
182 0x6C, // R.D.
183 0x74, // R.DU
184 0x71, // RL..
185 0x76, // RL.U
186 0x77, // RLD.
187 0x6E, // RLDU
189 return (flags&Flag.GraphMask ? transTbl.ptr[flags&Flag.GraphMask] : 0);
191 void g1line(bool setattr) (ubyte gf, ubyte afg, ubyte abg) nothrow @trusted @nogc {
192 if (gf&Flag.GraphMask) {
193 if (flags&Flag.G1) {
194 if ((flags&Flag.GraphMask) == 0) {
195 // check if we have some line drawing char here
196 switch (ch) {
197 case 0x6A: flags |= Flag.GraphLeft|Flag.GraphUp; break;
198 case 0x6B: flags |= Flag.GraphLeft|Flag.GraphDown; break;
199 case 0x6C: flags |= Flag.GraphRight|Flag.GraphDown; break;
200 case 0x6D: flags |= Flag.GraphRight|Flag.GraphUp; break;
201 case 0x6E: flags |= Flag.GraphLeft|Flag.GraphRight|Flag.GraphUp|Flag.GraphDown; break;
202 case 0x71: flags |= Flag.GraphLeft|Flag.GraphRight; break;
203 case 0x74: flags |= Flag.GraphUp|Flag.GraphDown|Flag.GraphRight; break;
204 case 0x75: flags |= Flag.GraphUp|Flag.GraphDown|Flag.GraphLeft; break;
205 case 0x76: flags |= Flag.GraphLeft|Flag.GraphRight|Flag.GraphUp; break;
206 case 0x77: flags |= Flag.GraphLeft|Flag.GraphRight|Flag.GraphDown; break;
207 case 0x78: flags |= Flag.GraphUp|Flag.GraphDown; break;
208 default:
211 } else {
212 flags &= ~Flag.GraphMask;
214 flags |= (gf&Flag.GraphMask)|Flag.G1;
215 ch = g1char;
216 } else if (flags&Flag.G1) {
217 flags &= ~(Flag.GraphMask|Flag.G1); // reset graphics
218 ch = ' ';
220 static if (setattr) {
221 fg = afg;
222 bg = abg;
226 static assert(Glyph.sizeof == 4);
229 // ////////////////////////////////////////////////////////////////////////// //
230 __gshared int ttycx, ttycy; // current cursor position (0-based)
231 __gshared Glyph[] ttywb; // working buffer
232 __gshared Glyph[] ttybc; // previous buffer
233 __gshared ubyte curFG = 7, curBG = 0;
234 __gshared bool ttzFullRefresh = true;
237 // ////////////////////////////////////////////////////////////////////////// //
238 __gshared ubyte* ttySavedBufs;
239 __gshared uint ttySBUsed;
240 __gshared uint ttySBSize;
242 private align(1) struct TxSaveInfo {
243 align(1):
244 int cx, cy; // cursor position
245 ubyte fg, bg; // current color
246 int x, y, w, h; // saved data (if any)
247 Glyph[0] data;
250 shared static ~this () {
251 import core.stdc.stdlib : free;
252 if (ttySavedBufs !is null) free(ttySavedBufs);
256 private ubyte* ttzSBAlloc (uint size) {
257 import core.stdc.stdlib : realloc;
258 import core.stdc.string : memset;
259 assert(size > 0);
260 if (size >= 4*1024*1024) assert(0, "wtf?!");
261 uint nsz = ttySBUsed+size;
262 if (nsz > ttySBSize) {
263 if (nsz&0xfff) nsz = (nsz|0xfff)+1;
264 auto nb = cast(ubyte*)realloc(ttySavedBufs, nsz);
265 if (nb is null) assert(0, "out of memory"); //FIXME
266 ttySavedBufs = nb;
267 ttySBSize = nsz;
269 assert(ttySBSize-ttySBUsed >= size);
270 auto res = ttySavedBufs+ttySBUsed;
271 ttySBUsed += size;
272 memset(res, 0, size);
273 return res;
277 // push area contents and cursor position (unaffected by scissors)
278 public void xtPushArea (int x, int y, int w, int h) {
279 if (w < 1 || h < 1 || x >= ttywIntr || y >= ttyhIntr) { x = y = w = h = 0; }
280 if (w > 0) {
281 int x0 = x, y0 = y, x1 = x+w-1, y1 = y+h-1;
282 if (x0 < 0) x0 = 0; else if (x0 >= ttywIntr) x0 = ttywIntr-1;
283 if (x1 < 0) x1 = 0; else if (x1 >= ttywIntr) x1 = ttywIntr-1;
284 if (y0 < 0) y0 = 0; else if (y0 >= ttyhIntr) y0 = ttyhIntr-1;
285 if (y1 < 0) y1 = 0; else if (y1 >= ttyhIntr) y1 = ttyhIntr-1;
286 if (x0 <= x1 && y0 <= y1) {
287 x = x0;
288 y = y0;
289 w = x1-x0+1;
290 h = y1-y0+1;
291 } else {
292 x = y = w = h = 0;
295 if (w < 1 || h < 1) { x = y = w = h = 0; }
296 uint sz = cast(uint)(TxSaveInfo.sizeof+Glyph.sizeof*w*h);
297 auto buf = ttzSBAlloc(sz+4);
298 auto st = cast(TxSaveInfo*)buf;
299 st.cx = ttycx;
300 st.cy = ttycy;
301 st.fg = curFG;
302 st.bg = curBG;
303 st.x = x;
304 st.y = y;
305 st.w = w;
306 st.h = h;
307 if (w > 0 && h > 0) {
308 assert(x >= 0 && y >= 0 && x < ttywIntr && y < ttyhIntr && x+w <= ttywIntr && y+h <= ttyhIntr);
309 import core.stdc.string : memcpy;
310 auto src = ttywb.ptr+y*ttywIntr+x;
311 auto dst = st.data.ptr;
312 foreach (immutable _; 0..h) {
313 memcpy(dst, src, Glyph.sizeof*w);
314 src += ttywIntr;
315 dst += w;
318 buf += sz;
319 *cast(uint*)buf = sz;
323 // pop (restore) area contents and cursor position (unaffected by scissors)
324 public void xtPopArea () {
325 if (ttySBUsed == 0) return;
326 assert(ttySBUsed >= 4);
327 auto sz = *cast(uint*)(ttySavedBufs+ttySBUsed-4);
328 ttySBUsed -= sz+4;
329 auto st = cast(TxSaveInfo*)(ttySavedBufs+ttySBUsed);
330 ttycx = st.cx;
331 ttycy = st.cy;
332 curFG = st.fg;
333 curBG = st.bg;
334 auto x = st.x;
335 auto y = st.y;
336 auto w = st.w;
337 auto h = st.h;
338 if (w > 0 && h > 0) {
339 assert(x >= 0 && y >= 0 && x < ttywIntr && y < ttyhIntr && x+w <= ttywIntr && y+h <= ttyhIntr);
340 import core.stdc.string : memcpy;
341 auto src = st.data.ptr;
342 auto dst = ttywb.ptr+y*ttywIntr+x;
343 foreach (immutable _; 0..h) {
344 memcpy(dst, src, Glyph.sizeof*w);
345 src += w;
346 dst += ttywIntr;
352 // ////////////////////////////////////////////////////////////////////////// //
353 // initialize system, allocate buffers, clear screen, etc
354 public void xtInit () {
355 import core.sys.posix.unistd : write;
356 weAreFucked = ttyIsUtfucked;
357 setupWinch();
358 ttywIntr = ttyWidth;
359 ttyhIntr = ttyHeight;
360 ttywb.length = ttywIntr*ttyhIntr;
361 ttybc.length = ttywIntr*ttyhIntr;
362 ttywb[] = Glyph.init;
363 ttybc[] = Glyph.init;
364 ttycx = ttycy = 0;
365 ttyzScissor = XtScissor.fullscreen;
366 // clear screen
367 enum initStr =
368 "\x1b[?1034l"~ // xterm: disable "eightBitInput"
369 "\x1b[?1036h"~ // xterm: enable "metaSendsEscape"
370 "\x1b[?1039h"~ // xterm: enable "altSendsEscape"
371 // disable various mouse reports
372 "\x1b[?1000l"~
373 "\x1b[?1001l"~
374 "\x1b[?1002l"~
375 "\x1b[?1003l"~
376 "\x1b[?1004l"~ // don't send focus events
377 "\x1b[?1005l"~
378 "\x1b[?1006l"~
379 "\x1b[?1015l"~
380 "\x1b[?1000l";
381 write(1, initStr.ptr, initStr.length);
382 xtFullRefresh();
386 public bool xtNeedReinit () {
387 //return (ttywIntr != ttyWidth || ttyhIntr != ttyHeight);
388 return winSizeChanged;
392 // this will reset scissors
393 public void xtReinit () {
394 ttyzScissor = XtScissor.fullscreen;
395 if (ttywIntr != ttyWidth || ttyhIntr != ttyHeight) {
396 winSizeChanged = false;
397 ttywIntr = ttyWidth;
398 ttyhIntr = ttyHeight;
399 ttywb.length = ttywIntr*ttyhIntr;
400 ttybc.length = ttywIntr*ttyhIntr;
401 ttywb[] = Glyph.init;
402 ttybc[] = Glyph.init;
403 ttycx = ttycy = 0;
404 // clear screen
405 //enum initStr = "\x1b[H\x1b[0;37;40m\x1b[2J";
406 //write(1, initStr.ptr, initStr.length);
407 xtFullRefresh();
412 // ////////////////////////////////////////////////////////////////////////// //
413 __gshared char[128*1024] ttytbuf = 0;
414 __gshared int ttytpos;
415 __gshared int lastx, lasty;
418 private void ttzFlush () nothrow @nogc {
419 if (ttytpos > 0) {
420 import core.sys.posix.unistd : write;
421 write(1, ttytbuf.ptr, ttytpos);
422 ttytpos = 0;
427 private void ttzPut (const(char)[] str...) nothrow @nogc {
428 import core.stdc.string : memcpy;
429 while (str.length > 0) {
430 uint left = cast(uint)ttytbuf.length-ttytpos;
431 if (left == 0) { ttzFlush(); left = cast(uint)ttytbuf.length; }
432 if (left > str.length) left = cast(uint)str.length;
433 memcpy(ttytbuf.ptr+ttytpos, str.ptr, left);
434 ttytpos += left;
435 str = str[left..$];
440 private void ttzPutUInt (uint n) nothrow @nogc {
441 import core.stdc.string : memcpy;
442 char[64] ttbuf = void;
443 uint tbpos = cast(uint)ttbuf.length;
444 do {
445 ttbuf[--tbpos] = cast(char)(n%10+'0');
446 } while ((n /= 10) != 0);
447 ttzPut(ttbuf[tbpos..$]);
451 private void ttzPutUHex (uint n) nothrow @nogc {
452 char[8] ttbuf = void;
453 foreach_reverse (ref char ch; ttbuf) {
454 int d = n%16;
455 if (d < 10) d += '0'; else d += 'a'-10;
456 ch = cast(char)d;
457 n /= 16;
459 ttzPut(ttbuf[]);
463 // ////////////////////////////////////////////////////////////////////////// //
464 public void xtSetTerminalTitle (const(char)[] title) {
465 import core.sys.posix.unistd : write;
466 if (title.length > 500) title = title[0..500];
467 enum titStart = "\x1b]2;";
468 enum titEnd = "\x07";
469 //enum suffix = " -- egedit";
470 bool good = true;
471 foreach (char ch; title) if (ch < ' ' || ch == 127) { good = false; break; }
472 write(1, titStart.ptr, titStart.length);
473 if (good) {
474 if (title.length) write(1, title.ptr, title.length);
475 } else {
476 foreach (char ch; title) write(1, &ch, 1);
478 if (ttyzTitleSuffix.length) write(1, ttyzTitleSuffix.ptr, ttyzTitleSuffix.length);
479 write(1, titEnd.ptr, titEnd.length);
483 // ////////////////////////////////////////////////////////////////////////// //
484 // redraw the whole screen; doesn't flush it yet
485 public void xtFullRefresh () nothrow @trusted @nogc {
486 ttzFullRefresh = true;
490 // ////////////////////////////////////////////////////////////////////////// //
491 // this will reset scissors
492 public void xtFlush () /*nothrow @nogc*/ {
493 void gotoXY (int x, int y) {
494 if (x == lastx && y == lasty) return;
495 //debug { import iv.vfs.io; stderr.writeln("x=", x, "; y=", y, "; last=", lastx, "; lasty=", lasty); }
496 if (y == lasty) {
497 // move horizontally
498 if (x == 0) { ttzPut("\r"); lastx = 0; return; }
499 ttzPut("\x1b[");
500 if (x < lastx) {
501 // move cursor left
502 ttzPutUInt(lastx-x);
503 ttzPut("D");
504 } else if (x > lastx) {
505 // move cursor right
506 ttzPutUInt(x-lastx);
507 ttzPut("C");
509 lastx = x;
510 } else if (x == lastx) {
511 // move vertically
512 ttzPut("\x1b[");
513 if (y < lasty) {
514 // move cursor up
515 ttzPutUInt(lasty-y);
516 ttzPut("A");
517 } else if (y > lasty) {
518 // move cursor down
519 ttzPutUInt(y-lasty);
520 ttzPut("B");
522 lasty = y;
523 } else {
524 // move in both directions
525 //TODO: optimize this too
526 ttzPut("\x1b[");
527 ttzPutUInt(y+1);
528 ttzPut(";");
529 ttzPutUInt(x+1);
530 ttzPut("H");
531 lastx = x;
532 lasty = y;
536 ttyzScissor = XtScissor.fullscreen;
538 ubyte inG0G1 = 0;
539 ttytpos = 0; // reset output position
540 if (ttzFullRefresh) {
541 ttzPut("\x1b[H");
542 lastx = lasty = 0;
544 ttzPut("\x1b[0;38;5;7;48;5;0m");
545 ubyte lastFG = 7, lastBG = 0;
546 int tsz = ttywIntr*ttyhIntr;
547 // fix glyph chars
548 auto tsrc = ttywb.ptr; // source buffer
549 auto tdst = ttybc.ptr; // destination buffer
550 //immutable doctrans = (termType == TermType.linux);
551 enum doctrans = false;
552 for (uint pos = 0; pos < tsz; *tdst++ = *tsrc++, ++pos) {
553 if (tsrc.ch == '\t') { tsrc.ch = '\x62'; tsrc.flags = Glyph.Flag.G1; }
554 if (tsrc.ch == '\v') { tsrc.ch = '\x69'; tsrc.flags = Glyph.Flag.G1; }
555 if (tsrc.ch == '\n') { tsrc.ch = '\x65'; tsrc.flags = Glyph.Flag.G1; }
556 if (tsrc.ch == '\r') { tsrc.ch = '\x64'; tsrc.flags = Glyph.Flag.G1; }
557 else if (tsrc.ch == 0) { tsrc.ch = ' '; tsrc.flags = 0; }
558 else if (tsrc.ch < ' ' || tsrc.ch == 127) { tsrc.ch = '\x7e'; tsrc.flags = Glyph.Flag.G1; }
559 // skip things that doesn't need to be updated
560 if (!ttzFullRefresh) {
561 if (((tsrc.flags^tdst.flags)&Glyph.Flag.Mask) == 0 && tsrc.ch == tdst.ch && tsrc.bg == tdst.bg) {
562 // same char, different attrs? for spaces, it is enough to compare only bg color
563 // actually, some terminals may draw different colored cursor on different colored
564 // spaces, but i don't care: fix your terminal!
565 if (/*tsrc.ch == ' ' ||*/ tsrc.fg == tdst.fg) continue;
568 gotoXY(pos%ttywIntr, pos/ttywIntr);
569 if (inG0G1 != (tsrc.flags&Glyph.Flag.G1)) {
570 if ((inG0G1 = (tsrc.flags&Glyph.Flag.G1)) != 0) ttzPut('\x0e'); else ttzPut('\x0f');
572 // new attrs?
573 if (tsrc.bg != lastBG || (/*tsrc.ch != ' ' &&*/ tsrc.fg != lastFG)) {
574 ttzPut("\x1b[");
575 if (!doctrans) {
576 bool needSC = false;
577 if (tsrc.fg != lastFG) {
578 lastFG = tsrc.fg;
579 ttzPut("38;5;");
580 ttzPutUInt(lastFG);
581 needSC = true;
583 if (tsrc.bg != lastBG) {
584 lastBG = tsrc.bg;
585 if (needSC) ttzPut(';');
586 ttzPut("48;5;");
587 ttzPutUInt(lastBG);
589 } else {
590 lastFG = tsrc.fg;
591 lastBG = tsrc.bg;
592 ttzPut("0;");
593 auto c0 = tty2linux(lastFG);
594 auto c1 = tty2linux(lastBG);
595 if (c0 > 7) ttzPut("1;");
596 ttzPut("3");
597 ttzPutUInt(c0);
598 ttzPut(";4");
599 ttzPutUInt(c1);
601 ttzPut('m');
603 // draw char
604 if (inG0G1) {
605 ttzPut(tsrc.ch < 128 ? tsrc.ch : ' ');
606 } else if (!weAreFucked || tsrc.ch < 128) {
607 ttzPut(tsrc.ch);
608 } else {
609 char[8] ubuf = void;
610 dchar dch = koi2uni(cast(char)tsrc.ch);
611 auto len = utf8Encode(ubuf[], dch);
612 if (len < 1) { ubuf[0] = '?'; len = 1; }
613 ttzPut(ubuf[0..len]);
615 // adjust cursor position
616 if (++lastx == ttywIntr) {
617 lastx = 0;
618 ttzPut("\r");
621 // switch back to G0
622 if (inG0G1) ttzPut('\x0f');
623 // position cursor
624 gotoXY(ttycx, ttycy);
625 // done
626 ttzFlush();
627 ttzFullRefresh = false;
631 // ////////////////////////////////////////////////////////////////////////// //
632 public struct XtWindow {
633 private:
634 int x, y, w, h;
635 ushort fgbg;
637 nothrow @safe @nogc:
638 public:
639 this (int ax, int ay, int aw, int ah) @trusted {
640 x = ax;
641 y = ay;
642 w = aw;
643 h = ah;
644 fgbg = cast(ushort)((curFG<<8)|curBG); // with current color
647 static XtWindow fullscreen () @trusted { pragma(inline, true); return XtWindow(0, 0, ttywIntr, ttyhIntr); }
649 @property bool valid () const pure { pragma(inline, true); return (w > 0 && h > 0); }
650 // invalid windows are invisible ;-)
651 @property bool visible () const @trusted {
652 pragma(inline, true);
653 return
654 w > 0 && h > 0 && // valid
655 x < ttywIntr && y < ttyhIntr && // not too right/bottom
656 x+w > 0 && y+h > 0; // not too left/top
659 // clip this window to another window
660 //FIXME: completely untested (and unused!)
662 void clipBy() (in auto ref XtWindow ww) {
663 if (empty || ww.empty) { w = h = 0; return; }
664 if (x+w <= ww.x || y+h <= ww.y || x >= ww.x+ww.w || y >= ww.y+ww.h) { w = h = 0; return; }
665 // we are at least partially inside ww
666 if (x < ww.x) x = ww.x; // clip left
667 if (y < ww.y) y = ww.y; // clip top
668 if (x+w > ww.x+ww.w) w = ww.x+ww.w-x; // clip right
669 if (y+h > ww.y+ww.h) y = ww.y+ww.h-y; // clip bottom
673 @property int x0 () const pure { pragma(inline, true); return x; }
674 @property int y0 () const pure { pragma(inline, true); return y; }
675 @property int x1 () const pure { pragma(inline, true); return x+w-1; }
676 @property int y1 () const pure { pragma(inline, true); return y+h-1; }
677 @property int width () const pure { pragma(inline, true); return (w > 0 ? w : 0); }
678 @property int height () const pure { pragma(inline, true); return (h > 0 ? h : 0); }
680 @property void x0 (int v) pure { pragma(inline, true); x = v; }
681 @property void y0 (int v) pure { pragma(inline, true); y = v; }
682 @property void x1 (int v) pure { pragma(inline, true); w = (v-x0+1 > 0 ? v-x0+1 : 0); }
683 @property void y1 (int v) pure { pragma(inline, true); h = (v-y0+1 > 0 ? v-y0+1 : 0); }
684 @property void width (int v) pure { pragma(inline, true); w = v; }
685 @property void height (int v) pure { pragma(inline, true); h = v; }
687 @property ubyte fg () const pure { pragma(inline, true); return cast(ubyte)(fgbg>>8); }
688 @property ubyte bg () const pure { pragma(inline, true); return cast(ubyte)(fgbg&0xff); }
690 @property void fg (ubyte v) pure { pragma(inline, true); fgbg = cast(ushort)((fgbg&0x00ff)|(v<<8)); }
691 @property void bg (ubyte v) pure { pragma(inline, true); fgbg = cast(ushort)((fgbg&0xff00)|v); }
693 @property uint color () const pure { pragma(inline, true); return fgbg; }
694 @property void color (uint v) pure { pragma(inline, true); fgbg = cast(ushort)(v&0xffff); }
696 @property void fb (ubyte fg, ubyte bg) pure { pragma(inline, true); fgbg = cast(ushort)((fg<<8)|bg); }
698 void gotoXY (int x, int y) @trusted {
699 x += this.x;
700 y += this.y;
701 if (x >= this.x+this.w) x = this.x+this.w-1;
702 if (y >= this.y+this.h) y = this.y+this.h-1;
703 if (x < this.x) x = this.x;
704 if (y < this.y) y = this.y;
705 // now global
706 if (x < 0) x = 0;
707 if (y < 0) y = 0;
708 if (x >= ttywIntr) x = ttywIntr-1;
709 if (y > ttyhIntr) y = ttyhIntr-1;
710 // set new coords
711 ttycx = x;
712 ttycy = y;
715 // returns new length (can be 0, and both `x` and `y` are undefined in this case)
716 // ofs: bytes to skip (if x is "before" window
717 // x, y: will be translated to global coords
718 int normXYLen (ref int x, ref int y, int len, out int ofs) const @trusted {
719 if (len < 1 || w < 1 || h < 1 || x >= w || y < 0 || y >= h || !visible || !ttyzScissor.visible) return 0; // nothing to do
720 // crop to window
721 if (x < 0) {
722 if (x <= -len) return 0;
723 len += x;
724 ofs = -x;
725 x = 0;
727 int left = w-x;
728 if (left < len) len = left;
729 if (len < 1) return 0; // just in case
730 // crop to global space
731 x += this.x;
732 y += this.y;
733 if (x+len <= ttyzScissor.mX0 || x > ttyzScissor.x1 || y < ttyzScissor.mY0 || y > ttyzScissor.y1) return 0;
734 if (x < 0) {
735 if (x <= -len) return 0;
736 len += x;
737 ofs = -x;
738 x = 0;
740 if (x < ttyzScissor.mX0) {
741 auto z = ttyzScissor.mX0-x;
742 if (z >= len) return 0;
743 x += z;
744 len -= z;
746 if ((left = ttyzScissor.x1+1-x) < 1) return 0;
747 if (left < len) len = left;
748 return len;
751 int normXYLen (ref int x, ref int y, int len) const {
752 int ofs;
753 return normXYLen(x, y, len, ofs);
756 @trusted:
757 void writeHotStrAt (int x, int y, int w, const(char)[] s, uint hotattr, Align defalign=Align.Left, bool dohot=true, bool setcur=false) {
758 if (w < 1) return;
759 if (s.length && s.ptr[0] >= '\x01' && s.ptr[0] <= '\x03') {
760 defalign = cast(Align)s.ptr[0];
761 s = s[1..$];
763 if (s.length == 0) return;
764 if (s.length > int.max/32) s = s[0..int.max/32];
765 if (defalign < 1 || defalign > 3) defalign = Align.Left;
766 final switch (defalign) {
767 case Align.Left: break;
768 case Align.Right: x += w-hotStrLen(s, dohot); break;
769 case Align.Center: x += (w-hotStrLen(s, dohot))/2; break;
771 if (setcur) gotoXY(x, y);
772 if (dohot) {
773 // with hotchar
774 for (;;) {
775 import iv.strex : indexOf;
776 auto ampos = s.indexOf('&');
777 if (ampos < 0 || s.length-ampos < 2) { writeStrAt(x, y, s); break; }
778 if (ampos > 0) { writeStrAt(x, y, s[0..ampos]); x += cast(int)ampos; }
779 if (s.ptr[ampos+1] != '&') {
780 if (setcur) { gotoXY(x, y); setcur = false; }
781 auto oc = fgbg;
782 fgbg = hotattr&0xffff;
783 writeCharsAt(x, y, 1, s.ptr[ampos+1]);
784 fgbg = oc;
785 } else {
786 writeCharsAt(x, y, 1, '&');
788 ++x;
789 s = s[ampos+2..$];
791 } else {
792 // no hotchar
793 writeStrAt(x, y, s);
797 const:
798 void writeStrAt(bool g1=false) (int x, int y, const(char)[] str...) {
799 if (str.length > int.max) str = str[0..int.max];
800 int ofs;
801 auto len = normXYLen(x, y, cast(int)str.length, ofs);
802 if (len < 1) return;
803 immutable f = fg, b = bg;
804 auto src = cast(const(char)*)str.ptr+ofs;
805 auto dst = ttywb.ptr+y*ttywIntr+x;
806 while (len-- > 0) {
807 dst.fg = f;
808 dst.bg = b;
809 dst.ch = *src++;
810 static if (g1) {
811 dst.flags &= ~Glyph.Flag.GraphMask;
812 dst.flags |= Glyph.Flag.G1;
813 } else {
814 dst.flags &= ~(Glyph.Flag.GraphMask|Glyph.Flag.G1);
816 ++dst;
820 void writeCharAt(bool g1=false) (int x, int y, char ch) { writeCharsAt!g1(x, y, 1, ch); }
822 void writeCharsAt(bool g1=false) (int x, int y, int count, char ch) {
823 auto len = normXYLen(x, y, count);
824 if (len < 1) return;
825 immutable f = fg, b = bg;
826 auto dst = ttywb.ptr+y*ttywIntr+x;
827 while (len-- > 0) {
828 dst.fg = f;
829 dst.bg = b;
830 dst.ch = ch;
831 static if (g1) {
832 dst.flags &= ~Glyph.Flag.GraphMask;
833 dst.flags |= Glyph.Flag.G1;
834 } else {
835 dst.flags &= ~(Glyph.Flag.GraphMask|Glyph.Flag.G1);
837 ++dst;
841 void writeUIntAt (int x, int y, uint n) {
842 char[64] ttbuf = void;
843 uint tbpos = cast(uint)ttbuf.length;
844 do {
845 ttbuf[--tbpos] = cast(char)(n%10+'0');
846 } while ((n /= 10) != 0);
847 writeStrAt(x, y, ttbuf[tbpos..$]);
850 void writeUHexAt (int x, int y, uint n) {
851 char[8] ttbuf = void;
852 foreach_reverse (ref char ch; ttbuf) {
853 int d = n%16;
854 if (d < 10) d += '0'; else d += 'a'-10;
855 ch = cast(char)d;
856 n /= 16;
858 writeStrAt(x, y, ttbuf[]);
861 void hline(bool setattr=true) (int x, int y, int len) {
862 auto nlen = normXYLen(x, y, len);
863 if (nlen < 1) return;
864 immutable f = fg, b = bg;
865 if (nlen == 1) {
866 ttywb.ptr[y*ttywIntr+x].g1line!setattr(Glyph.Flag.GraphLeft|Glyph.Flag.GraphRight, f, b);
867 } else {
868 ttywb.ptr[y*ttywIntr+x].g1line!setattr(Glyph.Flag.GraphRight, f, b);
869 foreach (ref gl; ttywb.ptr[y*ttywIntr+x+1..y*ttywIntr+x+nlen-1]) gl.g1line!setattr(Glyph.Flag.GraphLeft|Glyph.Flag.GraphRight, f, b);
870 ttywb.ptr[y*ttywIntr+x+nlen-1].g1line!setattr(Glyph.Flag.GraphLeft, f, b);
874 void vline(bool setattr=true) (int x, int y, int len) {
875 if (x < 0 || x >= w || len < 1 || y >= h || w < 1 || h < 1) return;
876 bool first = true;
877 bool last = true;
878 if (y < 0) {
879 first = false;
880 if (y <= -len) return;
881 len += y;
882 y = 0;
884 if (len > h-y) { last = false; len = h-y; }
885 if (len < 1) return; // just in case
886 immutable f = fg, b = bg;
888 if (normXYLen(x, y, 1) != 1) return;
889 if (len == 1) {
890 ttywb.ptr[y*ttywIntr+x].g1line!setattr(Glyph.Flag.GraphUp|Glyph.Flag.GraphDown, f, b);
891 } else {
892 ttywb.ptr[y*ttywIntr+x].g1line!setattr(Glyph.Flag.GraphDown, f, b);
893 foreach (int sy; y+1..y+len-1) ttywb.ptr[sy*ttywIntr+x].g1line!setattr(Glyph.Flag.GraphUp|Glyph.Flag.GraphDown, f, b);
894 ttywb.ptr[(y+len-1)*ttywIntr+x].g1line!setattr(Glyph.Flag.GraphUp, f, b);
897 if (len == 1) {
898 if (normXYLen(x, y, 1) != 1) return;
899 ttywb.ptr[y*ttywIntr+x].g1line!setattr(Glyph.Flag.GraphUp|Glyph.Flag.GraphDown, f, b);
900 return;
902 while (len-- > 0) {
903 int xx = x, yy = y;
904 if (normXYLen(xx, yy, 1) == 1) {
905 if (first) {
906 first = false;
907 ttywb.ptr[yy*ttywIntr+xx].g1line!setattr(Glyph.Flag.GraphDown, f, b);
908 } else if (last && len == 0) {
909 ttywb.ptr[yy*ttywIntr+xx].g1line!setattr(Glyph.Flag.GraphUp, f, b);
910 } else {
911 ttywb.ptr[yy*ttywIntr+xx].g1line!setattr(Glyph.Flag.GraphUp|Glyph.Flag.GraphDown, f, b);
914 ++y;
918 void frame(bool filled=false) (int x, int y, int w, int h) {
919 if (w < 1 || h < 1) return;
920 // erase lines first
921 writeCharsAt(x, y, w, ' ');
922 foreach (immutable sy; y..y+h) {
923 writeCharsAt(x, sy, 1, ' ');
924 writeCharsAt(x+w-1, sy, 1, ' ');
926 writeCharsAt(x, y+h-1, w, ' ');
927 // draw lines
928 if (h == 1) { hline(x, y, w); return; }
929 if (w == 1) { vline(x, y, h); return; }
930 hline(x, y, w); // top
931 hline(x, y+h-1, w); // bottom
932 vline(x, y, h); // left
933 vline(x+w-1, y, h); // right
934 static if (filled) {
935 foreach (immutable sy; y+1..y+h-1) writeCharsAt(x+1, sy, w-2, ' ');
939 void hshadow (int x, int y, int len) {
940 static ubyte shadowColor (ubyte clr) nothrow @trusted @nogc {
941 ubyte r, g, b;
942 ttyColor2rgb(clr, r, g, b);
943 return ttyRgb2Color(r/3, g/3, b/3);
946 len = normXYLen(x, y, len);
947 if (len < 1) return;
948 auto dst = ttywb.ptr+y*ttywIntr+x;
949 while (len-- > 0) {
950 dst.fg = shadowColor(dst.fg);
951 dst.bg = shadowColor(dst.bg);
952 ++dst;
956 void shadowBox (int x, int y, int w, int h) {
957 if (w < 1) return;
958 while (h-- > 0) hshadow(x, y++, w);
961 void frameShadowed(bool filled=false) (int x, int y, int w, int h) {
962 if (w < 1 || h < 1) return;
963 frame!filled(x, y, w, h);
964 shadowBox(x+w, y+1, 2, h-1);
965 hshadow(x+2, y+h, w);
968 void fill(bool g1=false) (int x, int y, int w, int h, char ch=' ') {
969 foreach (immutable sy; y..y+h) writeCharsAt!g1(x, sy, w, ch);
972 enum Align : char {
973 Left = '\x01',
974 Right = '\x02',
975 Center = '\x03',
978 static int hotStrLen (const(char)[] s, bool dohot=true) nothrow @trusted @nogc {
979 if (s.length > int.max) s = s[0..int.max];
980 int res = cast(int)s.length;
981 if (res && s.ptr[0] >= '\x01' && s.ptr[0] <= '\x03') --res; // left
982 if (dohot) {
983 for (;;) {
984 import iv.strex : indexOf;
985 auto ampos = s.indexOf('&');
986 if (ampos < 0 || s.length-ampos < 2) break;
987 --res;
988 s = s[ampos+2..$];
991 return res;
994 static char hotChar (const(char)[] s, bool dohot=true) nothrow @trusted @nogc {
995 if (dohot) {
996 if (s.length && s.ptr[0] >= '\x01' && s.ptr[0] <= '\x03') s = s[1..$];
997 for (;;) {
998 import iv.strex : indexOf;
999 auto ampos = s.indexOf('&');
1000 if (ampos < 0 || s.length-ampos < 2) return 0;
1001 if (s.ptr[ampos+1] != '&') return s.ptr[ampos+1];
1002 s = s[ampos+2..$];
1005 return 0;
1009 static int hotCharOfs (const(char)[] s, bool dohot=true) nothrow @trusted @nogc {
1010 if (dohot) {
1011 if (s.length && s.ptr[0] >= '\x01' && s.ptr[0] <= '\x03') s = s[1..$];
1012 int ofs = 0;
1013 for (;;) {
1014 import iv.strex : indexOf;
1015 auto ampos = s.indexOf('&');
1016 if (ampos < 0 || s.length-ampos < 2) return 0;
1017 if (s.ptr[ampos+1] != '&') return ofs+cast(int)ampos;
1018 s = s[ampos+2..$];
1021 return 0;
1027 // ////////////////////////////////////////////////////////////////////////// //
1028 private __gshared int screenSwapped = 0;
1031 public void altScreen () nothrow @nogc {
1032 if (++screenSwapped == 1) {
1033 import core.sys.posix.unistd : write;
1034 enum initStr =
1035 "\x1b[?1048h"~ // save cursor position
1036 "\x1b[?1047h"~ // set alternate screen
1037 "\x1b[?7l"~ // turn off autowrapping
1038 "\x1b[?25h"~ // make cursor visible
1039 "\x1b(B"~ // G0 is ASCII
1040 "\x1b)0"~ // G1 is graphics
1041 "\x1b[?2004h"~ // enable bracketed paste
1042 "\x1b[?1000h\x1b[?1006h\x1b[?1002h"~ // SGR mouse reports
1044 write(1, initStr.ptr, initStr.length);
1045 xtFullRefresh();
1050 public void normalScreen () @trusted nothrow @nogc {
1051 if (--screenSwapped == 0) {
1052 import core.sys.posix.unistd : write;
1053 enum deinitStr =
1054 "\x1b[?1047l"~ // set normal screen
1055 "\x1b[?7h"~ // turn on autowrapping
1056 "\x1b[0;37;40m"~ // set 'normal' attributes
1057 "\x1b[?1048l"~ // restore cursor position
1058 "\x1b[?25h"~ // make cursor visible
1059 "\x1b[?2004l"~ // disable bracketed paste
1060 "\x1b[?1002l\x1b[?1006l\x1b[?1000l"~ // disable mouse reports
1062 write(1, deinitStr.ptr, deinitStr.length);
1063 xtFullRefresh();
1068 // ////////////////////////////////////////////////////////////////////////// //
1069 private extern(C) void ttyzAtExit () {
1070 if (screenSwapped) {
1071 screenSwapped = 1;
1072 normalScreen();
1076 shared static this () {
1077 import core.stdc.stdlib : atexit;
1078 atexit(&ttyzAtExit);