egra: progress bar rendering optimisations
[iv.d.git] / vt100_sample / simpleterm.d
blobcff119a94a1421c8a83044daf2be584a68c3e15e
1 /* coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
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 module simpleterm;
17 private:
19 import core.time : MonoTime, Duration;
21 import iv.strex;
22 import iv.termrgb;
23 import iv.utfutil;
24 import iv.vfs.io;
25 import iv.vt100;
26 import iv.x11;
29 // ////////////////////////////////////////////////////////////////////////// //
30 enum fontNorm = "-*-terminus-bold-*-*-*-20-*-*-*-*-*-*-*";
31 enum fontBold = "-*-terminus-bold-*-*-*-20-*-*-*-*-*-*-*";
33 enum OptPtrBlankTime = 1500;
34 enum OptPtrBlinkTime = 700;
37 // ////////////////////////////////////////////////////////////////////////// //
38 class ExitException : Exception { this () { super("exit"); } }
39 class ExitError : ExitException { this () { super(); } }
42 void die(AA...) (string fmt, AA args) {
43 stderr.write("FATAL: ");
44 stderr.writeln(fmt, args);
45 throw new ExitError();
49 // ////////////////////////////////////////////////////////////////////////// //
50 public bool isUTF8Locale () nothrow @trusted @nogc {
51 import core.atomic;
53 static char tolower (char ch) pure nothrow @safe @nogc { return (ch >= 'A' && ch <= 'Z' ? cast(char)(ch-'A'+'a') : ch); }
55 __gshared bool res = false;
56 __gshared bool checked = false;
57 static shared int waiting = 0; // 0: not inited; 1: initializing; 2: done
59 if (!checked) {
60 if (cas(&waiting, 0, 1)) {
61 // not inited -> inited
62 import core.stdc.locale;
63 char* lct = setlocale(LC_CTYPE, null);
64 if (lct !is null) {
65 //auto lang = getenv("LANG");
66 //if (lang is null) return;
67 auto lang = lct;
68 while (*lang) {
69 if (tolower(lang[0]) == 'u' && tolower(lang[1]) == 't' && tolower(lang[2]) == 'f') { res = true; break; }
70 ++lang;
72 //res = (strcasestr(lct, "utf") !is null);
73 } else {
74 res = false;
76 checked = true;
77 // just in case
78 atomicFence();
79 if (!cas(&waiting, 1, 2)) assert(0, "wtf?!");
80 } else {
81 // either inited, or initializing
82 while (atomicLoad(waiting) != 2) {}
83 // just in case
84 atomicFence();
85 if (!checked) assert(0, "wtf?!");
88 return res;
92 // ////////////////////////////////////////////////////////////////////////// //
93 final class DiWindow {
94 private:
95 private import core.stdc.config : c_long;
97 private:
98 enum RedrawRateActive = 40; // each 40 msecs
99 enum RedrawRateInactive = 100; // each 100 msecs
101 enum DefaultBGColorIdx = 0;
102 enum DefaultFGColorIdx = 7;
103 enum ActiveCursorColorBGIdx = 256; // +1 for blink
104 enum ActiveCursorColorFGIdx = 258; // same for both blink stages
105 enum InactiveCursorColorFGIdx = 259; // same for both blink stages
106 enum BoldFGIdx = 260;
107 enum UnderlineFGIdx = 260;
109 enum { FONTDEF, FONTBOLD }
110 bool mOneFont; // FONTDEF is the same as FONTBOLD?
112 Window mXEmbed; // do xembed with this wid
114 int csigfd = -1; // signals fd
116 Display *dpy;
117 Colormap cmap;
118 Window win;
119 Cursor mXTextCursor; // text cursor
120 Cursor mXDefaultCursor; // 'default' cursor
121 Cursor mXBlankCursor;
122 XIM xim;
123 XIC xic;
124 int scr;
125 int winWidth; // window width in pixels
126 int winHeight; // window height in pixels
127 int charWidth; // char width
128 int charHeight; // char height
130 bool mForceRedraw; // force full terminal redraw
131 bool mForceDirtyRedraw; // force update of dirty areas
133 char[1024] mLastTitle = 0;
134 int mLastTitleLength = int.max;
135 MonoTime mLastTitleChangeTime;
137 bool mDoSelection;
138 int mSelLastX, mSelLastY; // last mouse position if mDoSelection is true
139 string mCurSelection;
141 // for blinking
142 MonoTime mCurLastPhaseChange;
143 MonoTime mCurLastMove;
144 int mCurPhase; // 0/1
146 MonoTime mLastInputTime; // for pointer blanking
147 bool mPointerVisible;
149 MonoTime mLastDrawTime;
151 enum {
152 wsVisible = 0x01,
153 wsFocused = 0x02,
155 ubyte mWinState; // focus, visible
157 @property bool isVisible () const pure @safe nothrow @nogc { pragma(inline, true); return ((mWinState&wsVisible) != 0); }
158 @property bool isFocused () const pure @safe nothrow @nogc { pragma(inline, true); return ((mWinState&wsFocused) != 0); }
160 Atom xaXEmbed;
161 Atom xaVTSelection;
162 Atom xaClipboard;
163 Atom xaUTF8;
164 Atom xaTargets;
165 Atom xaNetWMName;
166 Atom xaWMProtocols;
167 Atom xaWMDeleteWindow;
168 Atom xaTerminalMessage;
170 enum {
171 WinMsgDirty = 1,
174 // Drawing Context
175 enum { CLNORMAL, CLBW, CLGREEN }
176 c_ulong[512][3] clrs;
177 GC gc;
178 static struct XFont {
179 int ascent;
180 int descent;
181 short lbearing;
182 short rbearing;
183 XFontSet set;
184 Font fid;
186 XFont[2] font;
188 static final class DittyTab {
189 DiWindow xw;
190 VT100Emu appbuf;
191 bool prevcurVis = false;
192 int prevcurX, prevcurY;
193 bool reversed; // is everything reversed?
195 char[1024] mLastTitle = 0;
196 int mLastTitleLength = int.max;
197 MonoTime mLastTitleChangeTime;
199 char[1024] mLastProcess = 0;
200 int mLastProcessLength;
202 char[1024] mLastFullProcess = 0;
203 int mLastFullProcessLength;
205 this (DiWindow axw, int aw, int ah) {
206 import std.algorithm : max, min;
207 assert(axw !is null);
208 xw = axw;
209 aw = max(MinBufferWidth, min(aw, MaxBufferWidth));
210 ah = max(MinBufferHeight, min(ah, MaxBufferHeight));
211 // create application buffer
212 appbuf = new VT100Emu(aw, ah, isUTF8Locale);
213 appbuf.onScrollUp = &axw.onScrollUp;
214 appbuf.onScrollDown = &axw.onScrollDown;
215 appbuf.onBell = &axw.onBell;
216 appbuf.onNewTitleEvent = &axw.onNewTitleEvent;
217 appbuf.onReverseEvent = &axw.onReverseEvent;
220 @property ScreenBuffer activebuf () nothrow @safe @nogc { pragma(inline, true); return appbuf; }
221 // title is always 0-terminated
222 @property const(char)[] title () nothrow @safe @nogc { pragma(inline, true); return mLastTitle[0..mLastTitleLength]; }
223 @property const(char)[] appname () nothrow @safe @nogc { pragma(inline, true); return mLastProcess[0..mLastProcessLength]; }
224 @property const(char)[] fullappname () nothrow @safe @nogc { pragma(inline, true); return mLastFullProcess[0..mLastFullProcessLength]; }
226 void close () {
229 void onResize (int aw, int ah) {
230 appbuf.resize(aw, ah);
231 appbuf.setFullDirty();
234 void keypressEvent (dchar dch, KeySym ksym, X11ModState modstate) {
235 activebuf.keypressEvent(dch, ksym, modstate);
238 // return `true` if process name was changed
239 bool checkFixProcess () nothrow @nogc {
240 bool res = false;
241 char[1024] buf = void;
242 auto nm = appbuf.getFullProcessName(buf[]);
243 if (nm.length != mLastFullProcessLength || mLastFullProcess[0..mLastFullProcessLength] != nm[0..$]) {
244 mLastFullProcess[0..nm.length] = nm[];
245 mLastFullProcessLength = cast(int)nm.length;
247 nm = appbuf.getProcessName(buf[]);
248 if (nm.length != mLastProcessLength || mLastProcess[0..mLastProcessLength] != nm[0..$]) {
249 res = true;
250 mLastProcess[0..nm.length] = nm[];
251 mLastProcessLength = cast(int)nm.length;
252 setTitle(nm);
254 return res;
257 // return `true` if process name was changed
258 void setTitle (const(char)[] atitle) nothrow @nogc {
259 if (atitle.length > mLastTitle.length-1) atitle = atitle[0..mLastTitle.length-1]; //FIXME
260 mLastTitle[0..atitle.length] = atitle[];
261 mLastTitleLength = cast(int)atitle.length;
262 mLastTitle[mLastTitleLength] = 0;
266 DittyTab mTab;
268 final @property DittyTab tab () nothrow @safe @nogc { pragma(inline, true); return mTab; }
269 final @property VT100Emu appbuf () nothrow @safe @nogc { pragma(inline, true); return mTab.appbuf; }
270 final @property ScreenBuffer scrbuf () nothrow @safe @nogc { pragma(inline, true); return mTab.activebuf; }
272 dchar[MaxBufferWidth+8] mDrawBuf = void;
274 c_ulong getColor (usize idx) const nothrow @trusted @nogc {
275 //if (globalBW && (curterm == NULL || !curterm->blackandwhite)) return dc.clrs[globalBW][idx];
276 //if (curterm != NULL) return dc.clrs[curterm->blackandwhite%3][idx];
277 //return dc.clrs[0][idx];
278 return (idx < clrs[0].length ? clrs[CLNORMAL][idx] : clrs[0][0]);
281 //~this () { close(); } //oops
283 private:
284 void postMessage (c_long type, c_long l1=0, c_long l2=0, c_long l3=0, c_long l4=0) {
285 //{ import iv.writer; writeln("posting message..."); }
286 XEvent ev;
287 ev.type = ClientMessage;
288 ev.xclient.window = win;
289 ev.xclient.message_type = xaTerminalMessage;
290 ev.xclient.format = 32;
291 ev.xclient.data.l[0] = type;
292 ev.xclient.data.l[1] = l1;
293 ev.xclient.data.l[2] = l2;
294 ev.xclient.data.l[3] = l3;
295 ev.xclient.data.l[4] = l4;
296 XSendEvent(dpy, win, False, 0, &ev);
299 private:
300 void initAtoms () {
301 xaXEmbed = XInternAtom(dpy, "_XEMBED", False);
302 xaVTSelection = XInternAtom(dpy, "_STERM_SELECTION_", 0);
303 xaClipboard = XInternAtom(dpy, "CLIPBOARD", 0);
304 xaUTF8 = XInternAtom(dpy, "UTF8_STRING", 0);
305 xaNetWMName = XInternAtom(dpy, "_NET_WM_NAME", 0);
306 xaTargets = XInternAtom(dpy, "TARGETS", 0);
307 xaWMProtocols = XInternAtom(dpy, "WM_PROTOCOLS", 0);
308 xaWMDeleteWindow = XInternAtom(dpy, "WM_DELETE_WINDOW", 0);
309 xaTerminalMessage = XInternAtom(dpy, "K8_DITTY_MESSAGE", 0);
312 void xhints () {
313 XClassHint klass;
314 klass.res_class = "DiTTY_SAMPLE_TERMINAL";
315 klass.res_name = "DiTTY_SAMPLE_TERMINAL";
316 XWMHints wm;
317 wm.flags = InputHint;
318 wm.input = 1;
319 XSizeHints size;
320 size.flags = PMinSize|PMaxSize|PSize|PResizeInc|PBaseSize;
321 size.min_width = MinBufferWidth*charWidth;
322 size.min_height = (MinBufferHeight+1)*charHeight; // one more row for tabs and status info
323 size.max_width = MaxBufferWidth*charWidth;
324 size.max_height = (MaxBufferHeight+1)*charHeight;
325 size.width_inc = charWidth;
326 size.height_inc = charHeight;
327 /* if we'll do it this way, FluxBox will assume that minimal window size is equal to base size
328 size.width = winWidth;
329 size.height = winHeight;
330 size.base_width = winWidth;
331 size.base_height = winHeight;
333 size.width = size.min_width;
334 size.height = size.min_height;
335 size.base_width = size.min_width;
336 size.base_height = size.min_height;
337 XSetWMNormalHints(dpy, win, &size);
338 XSetWMProperties(dpy, win, null, null, null, 0, &size, &wm, &klass);
339 XSetWMProtocols(dpy, win, &xaWMDeleteWindow, 1);
340 XResizeWindow(dpy, win, winWidth, winHeight);
343 XFontSet xinitfont (string fontstr) {
344 import std.string : toStringz, fromStringz;
345 XFontSet set;
346 char *def;
347 char **missing;
348 int n;
349 missing = null;
350 set = XCreateFontSet(dpy, cast(char*)(fontstr.toStringz), &missing, &n, &def);
351 if (missing) {
352 //!!!:while (n--) errwriteln("diterm: missing fontset: ", missing[n].fromStringz);
353 XFreeStringList(missing);
355 return set;
358 void xgetfontinfo (XFontSet set, int *ascent, int *descent, short *lbearing, short *rbearing, Font *fid) {
359 import std.algorithm : max;
360 XFontStruct **xfonts; // owned by Xlib
361 char **fontNames; // owned by Xlib
362 *ascent = *descent = *lbearing = *rbearing = 0;
363 /*int n =*/ XFontsOfFontSet(set, &xfonts, &fontNames);
364 /*HACK: use only first font in set; this is due to possible extra-wide fonts*/
365 *fid = (*xfonts).fid;
366 *ascent = max(*ascent, (*xfonts).ascent);
367 *descent = max(*descent, (*xfonts).descent);
368 *lbearing = max(*lbearing, (*xfonts).min_bounds.lbearing);
369 *rbearing = max(*rbearing, (*xfonts).max_bounds.rbearing);
372 void initfonts (string fontstr, string bfontstr) {
373 import core.stdc.stdlib : malloc, free;
374 import core.stdc.string : strdup;
375 import core.stdc.locale;
376 char *olocale;
377 /* X Core Fonts fix */
378 /* sorry, we HAVE to do this shit here!
379 * braindamaged X11 will not work with utf-8 in Xmb*() correctly if we have, for example,
380 * koi8 as system locale unless we first create font set in system locale. don't even ask
381 * me why X.Org is so fuckin' broken. */
382 XFontSet tmp_fs;
383 char** missing;
384 int missing_count;
385 olocale = strdup(setlocale(LC_ALL, null)); /*FIXME: this can fail if we have no memory; fuck it*/
386 setlocale(LC_ALL, "");
387 tmp_fs = XCreateFontSet(dpy, cast(char*)"-*-*-*-*-*-*-*-*-*-*-*-*-*", &missing, &missing_count, null);
388 if (!tmp_fs) die("FATAL: can't apply workarount for X Core FontSets!");
389 if (missing) XFreeStringList(missing);
390 // throw out unused fontset
391 XFreeFontSet(dpy, tmp_fs);
392 //setlocale(LC_ALL, "ru_RU.utf-8");
393 //TODO: find suitable utf-8 encoding
394 setlocale(LC_ALL, "en_US.UTF-8");
395 // create fonts for utf-8 locale
396 if ((font[0].set = xinitfont(fontstr)) is null) {
397 /*if ((font[0].set = xinitfont(FONT)) is null)*/ die("can't load font %s", fontstr);
399 xgetfontinfo(font[0].set, &font[0].ascent, &font[0].descent, &font[0].lbearing, &font[0].rbearing, &font[0].fid);
400 if ((font[1].set = xinitfont(bfontstr)) is null) {
401 /*if ((font[1].set = xinitfont(FONTBOLD)) is null)*/ die("can't load font %s", bfontstr);
403 xgetfontinfo(font[1].set, &font[1].ascent, &font[1].descent, &font[1].lbearing, &font[1].rbearing, &font[1].fid);
405 if ((font[2].set = xinitfont(tabfont)) is null) {
406 /*if ((font[2].set = xinitfont(FONTTAB)) is null)*/ die("can't load font %s", tabfont);
408 xgetfontinfo(font[2].set, &font[2].ascent, &font[2].descent, &font[2].lbearing, &font[2].rbearing, &font[2].fid);
410 // restore locale
411 setlocale(LC_ALL, olocale);
412 free(olocale);
413 // same fonts for normal and bold?
414 mOneFont = (fontstr == bfontstr);
417 void xallocbwclr (usize idx, XColor *color) {
418 XQueryColor(dpy, cmap, color);
419 double lumi = 0.3*(cast(double)color.red/65535.0)+0.59*(cast(double)color.green/65535.0)+0.11*(cast(double)color.blue/65535.0);
420 color.red = color.green = color.blue = cast(ushort)(lumi*65535.0);
421 if (!XAllocColor(dpy, cmap, color)) {
422 import core.stdc.stdio;
423 stderr.fprintf("WARNING: could not allocate b/w color #%u\n", cast(uint)idx);
424 return;
426 clrs[CLBW][idx] = color.pixel;
427 color.red = color.blue = 0;
428 if (!XAllocColor(dpy, cmap, color)) {
429 import core.stdc.stdio;
430 stderr.fprintf("WARNING: could not allocate b/w color #%u\n", cast(uint)idx);
431 return;
433 clrs[CLGREEN][idx] = color.pixel;
436 void xallocnamedclr (usize idx, string cname) {
437 import std.string : toStringz;
438 XColor color;
439 if (!XAllocNamedColor(dpy, cmap, cname.toStringz, &color, &color)) {
440 import core.stdc.stdio;
441 stderr.fprintf("WARNING: could not allocate color #%u: '%.s'", cast(uint)idx, cast(uint)cname.length, cname.ptr);
442 return;
444 clrs[CLNORMAL][idx] = color.pixel;
445 xallocbwclr(idx, &color);
448 void xloadcols () {
449 static immutable string[16] defclr = [
450 // 8 normal colors
451 "#000000",
452 "#b21818",
453 "#18b218",
454 "#b26818",
455 "#1818b2",
456 "#b218b2",
457 "#18b2b2",
458 "#b2b2b2",
459 // 8 bright colors
460 "#686868",
461 "#ff5454",
462 "#54ff54",
463 "#ffff54",
464 "#5454ff",
465 "#ff54ff",
466 "#54ffff",
467 "#ffffff",
470 XColor color;
471 uint white = WhitePixel(dpy, scr);
472 clrs[0] = white;
473 clrs[1] = white;
474 clrs[2] = white;
476 // load colors [0-15]
477 foreach (immutable idx; 0..defclr.length) {
478 //const char *cname = opt_colornames[f]!=NULL?opt_colornames[f]:defcolornames[f];
479 xallocnamedclr(idx, defclr[idx]);
482 // load colors [256-...]
483 foreach (immutable idx; 256..clrs.length) xallocnamedclr(idx, "#fff700");
485 // load colors [16-255]; same colors as xterm
486 usize idx = 16;
487 foreach (immutable r; 0..6) {
488 foreach (immutable g; 0..6) {
489 foreach (immutable b; 0..6) {
490 color.red = cast(ushort)(r == 0 ? 0 : 0x3737+0x2828*r);
491 color.green = cast(ushort)(g == 0 ? 0 : 0x3737+0x2828*g);
492 color.blue = cast(ushort)(b == 0 ? 0 : 0x3737+0x2828*b);
493 if (!XAllocColor(dpy, cmap, &color)) {
494 import core.stdc.stdio;
495 stderr.fprintf("WARNING: could not allocate color #%u\n", cast(uint)idx);
496 } else {
497 clrs[CLNORMAL][idx] = color.pixel;
498 xallocbwclr(idx, &color);
500 ++idx;
504 foreach (immutable r; 0..24) {
505 color.red = color.green = color.blue = cast(ushort)(0x0808+0x0a0a*r);
506 if (!XAllocColor(dpy, cmap, &color)) {
507 import core.stdc.stdio;
508 stderr.fprintf("WARNING: could not allocate color #%u\n", cast(uint)idx);
509 } else {
510 clrs[CLNORMAL][idx] = color.pixel;
511 xallocbwclr(idx, &color);
513 ++idx;
515 assert(idx == 256);
516 // blinking cursor bg
517 xallocnamedclr(256, "#00ff00");
518 xallocnamedclr(257, "#00cc00");
519 // active cursor fg
520 xallocnamedclr(258, "#005500");
521 // inactive cursor
522 xallocnamedclr(259, "#009900");
523 // bold
524 xallocnamedclr(260, "#00afaf");
525 // underline
526 xallocnamedclr(261, "#00af00");
529 void changeTitle (const(char)[] s) nothrow @trusted @nogc {
530 if (s.length == 0) s = "";
531 usize len = s.length;
532 if (len >= mLastTitle.length) len = mLastTitle.length-1;
533 if (mLastTitleLength != len || mLastTitle[0..len] != s[0..len]) {
534 mLastTitle[0..len] = s[0..len];
535 mLastTitle[len] = 0;
536 mLastTitleLength = len;
537 XStoreName(dpy, win, mLastTitle.ptr);
538 XChangeProperty(dpy, win, xaNetWMName, xaUTF8, 8, PropModeReplace, cast(ubyte*)mLastTitle.ptr, cast(uint)mLastTitleLength);
539 XFlush(dpy); // immediate update
543 string atomName (Atom a) {
544 import std.string : fromStringz;
545 auto nm = XGetAtomName(dpy, a);
546 auto res = nm.fromStringz.idup;
547 XFree(nm);
548 return res;
551 // ////////////////////////////////////////////////////////////////////// //
552 private void setupSignals () {
553 if (csigfd < 0) {
554 import core.sys.linux.sys.signalfd : signalfd, SFD_NONBLOCK, SFD_CLOEXEC;
555 import core.sys.posix.signal : sigset_t, sigemptyset, sigaddset, sigprocmask;
556 import core.sys.posix.signal : SIG_BLOCK, SIGTERM, SIGHUP, SIGQUIT, SIGINT, SIGCHLD;
557 sigset_t mask;
558 sigemptyset(&mask);
559 //sigaddset(&mask, SIGTERM);
560 //sigaddset(&mask, SIGHUP);
561 //sigaddset(&mask, SIGQUIT);
562 //sigaddset(&mask, SIGINT);
563 sigaddset(&mask, SIGCHLD);
564 sigprocmask(SIG_BLOCK, &mask, null); // we block the signals
565 //pthread_sigmask(SIG_BLOCK, &mask, NULL); // we block the signal
566 csigfd = signalfd(-1, &mask, SFD_NONBLOCK|SFD_CLOEXEC);
567 if (csigfd < 0) die("can't setup signal handler");
571 void processDeadChildren () {
572 import core.sys.linux.sys.signalfd : signalfd_siginfo;
573 import core.sys.posix.unistd : read;
574 signalfd_siginfo si = void;
575 while (read(csigfd, &si, si.sizeof) > 0) {} // ignore errors here
576 mainloop: for (;;) {
577 import core.sys.posix.sys.wait : waitpid, WNOHANG, WIFEXITED, WEXITSTATUS;
578 int status;
579 auto pid = waitpid(-1, &status, WNOHANG);
580 if (pid <= 0) break; // no more dead children
581 if (tab.appbuf.checkDeadChild(pid, WEXITSTATUS(status))) continue mainloop;
582 if (onDeadChildren !is null) onDeadChildren(cast(int)pid, WEXITSTATUS(status));
586 import core.sys.posix.sys.types : pid_t;
587 static assert(pid_t.sizeof == int.sizeof);
589 private:
590 void onScrollUp (ScreenBuffer self, int y0, int y1, int count, bool wasDirty) nothrow {
591 if (!isVisible) return;
592 if (wasDirty) return;
593 bool restcur = (tab.prevcurVis && tab.prevcurX >= 0 && tab.prevcurX < self.width && tab.prevcurY >= y0 && tab.prevcurY <= y1);
594 if (restcur) undrawCursor();
595 xcopytty(
596 0, y0, // dest
597 0, y0+count,
598 self.width, y1-y0+1-count);
599 if (restcur) drawCursor();
600 // mark scrolled lines non-dirty
601 //{ import core.stdc.stdio; printf("from %d to %d (y0=%d; y1=%d)\n", y0, y1-count+1-1, y0, y1); }
602 foreach (immutable y; y0..y1-count+1) self.resetDirtyLine(y);
605 void onScrollDown (ScreenBuffer self, int y0, int y1, int count, bool wasDirty) nothrow {
606 if (!isVisible) return;
607 if (wasDirty) return;
608 // down
609 bool restcur = (tab.prevcurVis && tab.prevcurX >= 0 && tab.prevcurX < self.width && tab.prevcurY >= y0 && tab.prevcurY <= y1);
610 if (restcur) undrawCursor();
611 xcopytty(
612 0, y0+count, // dest
613 0, y0,
614 self.width, y1-y0+1-count);
615 if (restcur) drawCursor();
616 // mark scrolled lines non-dirty
617 foreach (immutable y; y0+count..y1+1) self.resetDirtyLine(y);
620 // ring a bell
621 final void onBell (ScreenBuffer self) nothrow @trusted @nogc {
622 XBell(dpy, 100);
625 // new title was set; we don't check if it's the same as old title
626 final void onNewTitleEvent (ScreenBuffer self, const(char)[] title) nothrow {
627 if (self is tab.appbuf) {
628 tab.checkFixProcess();
629 tab.setTitle(title);
630 changeTitle(title);
631 return;
635 // inverse mode changed; it should be in effect immediately
636 final void onReverseEvent (ScreenBuffer self) nothrow @trusted @nogc {
637 if (self is tab.appbuf) {
638 tab.reversed = !tab.reversed;
639 self.setFullDirty();
640 return;
644 public:
645 void delegate (int pid, int exitcode) onDeadChildren;
647 public:
648 // width and height in cells
649 this (int awdt, int ahgt) {
650 mTab = new DittyTab(this, awdt, ahgt);
651 setupSignals();
652 initialize(mTab.appbuf.width, mTab.appbuf.height);
655 private void initialize (int awdt, int ahgt) {
656 if (win) assert(0, "already initialized");
658 XSetWindowAttributes attrs;
659 Window parent;
660 XColor blackcolor;// = { 0, 0, 0, 0, 0, 0 };
661 int wwdt, whgt;
664 import core.stdc.locale;
665 import core.stdc.stdlib : free;
666 import core.stdc.string : strdup;
667 auto olocale = strdup(setlocale(LC_ALL, null));
668 //{ import std.string : fromStringz; import iv.writer; writeln("[", olocale.fromStringz, "]"); }
669 if (isUTF8Locale()) {
670 if (!setlocale(LC_ALL, "")) die("can't set locale");
671 } else {
672 if (!setlocale(LC_ALL, "en_US.UTF-8")) die("can't set UTF locale");
674 if (!XSupportsLocale()) die("X server doesn't support locale");
675 if (XSetLocaleModifiers("@im=local") is null) die("XSetLocaleModifiers failed");
676 setlocale(LC_ALL, olocale);
677 free(olocale);
680 dpy = XOpenDisplay();
681 if (!dpy) die("can't open display");
682 scr = XDefaultScreen(dpy);
684 initAtoms();
686 // fonts
687 initfonts(fontNorm, fontBold);
688 /* XXX: Assuming same size for bold font */
689 charWidth = font[0].rbearing-font[0].lbearing;
690 charHeight = font[0].ascent+font[0].descent;
691 //tch = font[2].ascent+font[2].descent;
693 // colors
694 cmap = XDefaultColormap(dpy, scr);
695 xloadcols();
697 // window - default size
698 wwdt = awdt*charWidth;
699 whgt = ahgt*charHeight;
701 winWidth = wwdt;
702 winHeight = whgt; // one more row for tabs and status info
704 attrs.background_pixel = None;//getColor(/*defaultBG*/0);
705 attrs.border_pixel = None;//getColor(/*defaultBG*/0);
706 attrs.bit_gravity = NorthWestGravity;
707 attrs.event_mask =
708 FocusChangeMask|
709 KeyPressMask|KeyReleaseMask|KeymapStateMask|
710 ExposureMask|VisibilityChangeMask|StructureNotifyMask|
711 ButtonMotionMask|
712 PointerMotionMask| //TODO: do we need motion reports at all?
713 ButtonPressMask|ButtonReleaseMask|
714 /*EnterWindowMask|LeaveWindowMask|*/
716 attrs.colormap = cmap;
717 parent = XRootWindow(dpy, scr);
718 if (mXEmbed) parent = mXEmbed;
719 win = XCreateWindow(dpy, parent, 0, 0,
720 winWidth, winHeight, 0, XDefaultDepth(dpy, scr), InputOutput,
721 XDefaultVisual(dpy, scr),
722 CWBackPixel|CWBorderPixel|CWBitGravity|CWEventMask|CWColormap,
723 &attrs);
724 xhints();
726 // input method
727 // force locale for correct XIM
729 import core.stdc.locale;
730 import core.stdc.stdlib : free;
731 import core.stdc.string : strdup;
732 auto olocale = strdup(setlocale(LC_ALL, null));
733 //{ import std.string : fromStringz; import iv.writer; writeln("[", olocale.fromStringz, "]"); }
734 if (isUTF8Locale()) {
735 if (!setlocale(LC_ALL, "")) die("can't set locale");
736 } else {
737 if (!setlocale(LC_ALL, "en_US.UTF-8")) die("can't set UTF locale");
740 if ((xim = XOpenIM(dpy, null, null, null)) is null) {
741 if (XSetLocaleModifiers("@im=local") is null) die("XSetLocaleModifiers failed");
742 if ((xim = XOpenIM(dpy, null, null, null)) is null) {
743 if (XSetLocaleModifiers("@im=") is null) die("XSetLocaleModifiers failed");
744 if ((xim = XOpenIM(dpy, null, null, null)) is null) die("XOpenIM() failed");
747 xic = XCreateIC(xim,
748 XNInputStyle.ptr, XIMPreeditNothing|XIMStatusNothing,
749 XNClientWindow.ptr, win,
750 XNFocusWindow.ptr, win,
751 null);
752 if (xic is null) die("XCreateIC failed");
754 setlocale(LC_ALL, olocale);
755 free(olocale);
758 // gc
759 gc = XCreateGC(dpy, cast(Drawable)win, 0, null);
760 XSetGraphicsExposures(dpy, gc, True);
762 mXTextCursor = XCreateFontCursor(dpy, XC_xterm);
763 //mXTextCursor = XCreateFontCursor(dpy, XC_arrow);
764 /* green cursor, black outline */
766 XRecolorCursor(dpy, mXTextCursor,
767 &(XColor){.red = 0x0000, .green = 0xffff, .blue = 0x0000},
768 &(XColor){.red = 0x0000, .green = 0x0000, .blue = 0x0000});
770 mXDefaultCursor = XCreateFontCursor(dpy, /*XC_X_cursor*/XC_left_ptr);
772 XRecolorCursor(dpy, mXDefaultCursor,
773 &(XColor){.red = 0x0000, .green = 0xffff, .blue = 0x0000},
774 &(XColor){.red = 0x0000, .green = 0x0000, .blue = 0x0000});
776 XDefineCursor(dpy, win, mXTextCursor);
777 XStoreName(dpy, win, "Ditty");
779 XSetForeground(dpy, gc, 0);
781 XMapWindow(dpy, win);
783 version(BLANKPTR_USE_GLYPH_CURSOR) {
784 mXBlankCursor = XCreateGlyphCursor(dpy, font[0].fid, font[0].fid, ' ', ' ', &blackcolor, &blackcolor);
785 } else {
786 char[1] cmbmp = 0;
787 Pixmap pm;
788 pm = XCreateBitmapFromData(dpy, cast(Drawable)win, cmbmp.ptr, 1, 1);
789 mXBlankCursor = XCreatePixmapCursor(dpy, pm, pm, &blackcolor, &blackcolor, 0, 0);
790 XFreePixmap(dpy, pm);
793 //XSetICFocus(xic);
795 XSync(dpy, 0);
798 void close () {
799 tab.close();
800 if (win) {
801 XFreeGC(dpy, gc);
802 XUnmapWindow(dpy, win);
803 XDestroyWindow(dpy, win);
804 win = cast(Window)0;
806 if (dpy) {
807 XCloseDisplay(dpy);
808 dpy = null;
812 // ////////////////////////////////////////////////////////////////////// //
813 void blankPointer () {
814 if (mPointerVisible && mXBlankCursor != None) {
815 mPointerVisible = false;
816 XDefineCursor(dpy, win, mXBlankCursor);
817 XFlush(dpy);
821 void unblankPointer () {
822 if (!mPointerVisible && mXTextCursor != None) {
823 mPointerVisible = true;
824 XDefineCursor(dpy, win, mXTextCursor);
825 XFlush(dpy);
827 mLastInputTime = MonoTime.currTime;
830 // ////////////////////////////////////////////////////////////////////// //
831 void drawString (int x, int y, const(char)[] str, usize fontset=FONTDEF) {
832 if (charHeight < 1 || charWidth < 1) return;
833 if (y < 0 || y*charHeight >= winHeight || str.length == 0 || x/charWidth >= winWidth) return;
834 Utf8DecoderFast dc;
835 int dlen = 0;
836 foreach (char ch; str) {
837 if (dc.decodeSafe(ch)) {
838 if (dlen == mDrawBuf.length) break;
839 mDrawBuf.ptr[dlen++] = dc.codepoint;
842 //{ import core.stdc.stdio; stderr.fprintf("dlen=%d\n", dlen); }
843 auto xbuf = mDrawBuf[0..dlen];
844 if (dlen == 0) return;
845 if (x < 0) {
846 x = -x;
847 if (x >= dlen) return;
848 xbuf = xbuf[x..$];
849 x = 0;
850 if (dlen == 0) return;
852 XFontSet xfontset = font[fontset].set;
853 int winx = x*charWidth;
854 int winy = y*charHeight+font[fontset].ascent;
855 // XwcDrawString will not fill background
856 XwcDrawImageString(dpy, cast(Drawable)win, xfontset, gc, winx, winy, xbuf.ptr, cast(uint)dlen);
859 void drawString (int x, int y, const(dchar)[] str, usize fontset=FONTDEF) {
860 if (charHeight < 1 || charWidth < 1) return;
861 if (y < 0 || y*charHeight >= winHeight || str.length == 0 || x/charWidth >= winWidth) return;
862 if (x < 0) {
863 x = -x;
864 if (x >= str.length) return;
865 str = str[x..$];
866 x = 0;
868 XFontSet xfontset = font[fontset].set;
869 int winx = x*charWidth;
870 int winy = y*charHeight+font[fontset].ascent;
871 // XwcDrawString will not fill background
872 XwcDrawImageString(dpy, cast(Drawable)win, xfontset, gc, winx, winy, str.ptr, cast(uint)str.length);
875 // ////////////////////////////////////////////////////////////////////// //
876 void drawTermChar (int cx, int cy, bool asCursor=false) nothrow @trusted @nogc {
877 if (cx >= 0 && cy >= 0 && cx < scrbuf.width && cy < scrbuf.height) {
878 auto gl = scrbuf[cx, cy];
879 ushort bg, fg, fontidx;
880 getAttrBGFGFont(gl.attr, tab.reversed, bg, fg, fontidx);
881 if (asCursor && isFocused) {
882 ushort cbg = cast(ushort)(ActiveCursorColorBGIdx+mCurPhase);
883 ushort cfg = ActiveCursorColorFGIdx;
884 if (cbg < clrs[0].length) bg = cbg;
885 if (cfg < clrs[0].length) fg = cfg;
886 } else {
887 if (scrbuf.isInSelection(cx, cy)) {
888 immutable t = bg;
889 bg = fg;
890 fg = t;
893 XSetBackground(dpy, gc, getColor(bg));
894 XSetForeground(dpy, gc, getColor(fg));
895 XFontSet xfontset = font[fontidx].set;
896 auto ascent = font[fontidx].ascent;
897 mDrawBuf.ptr[0] = gl.ch;
898 XwcDrawImageString(dpy, cast(Drawable)win, xfontset, gc, cx*charWidth, cy*charHeight+ascent, mDrawBuf.ptr, 1);
899 // draw rect
900 if (asCursor && !isFocused && InactiveCursorColorFGIdx < clrs[0].length) {
901 XSetForeground(dpy, gc, getColor(InactiveCursorColorFGIdx));
902 XDrawRectangle(dpy, cast(Drawable)win, gc, cx*charWidth, cy*charHeight, charWidth-1, charHeight-1);
904 scrbuf.setDirtyAt(cx, cy, false);
908 // ////////////////////////////////////////////////////////////////////// //
909 void getAttrBGFGFont() (in Attr a, bool reversed, out ushort bg, out ushort fg, out ushort fontidx) {
910 fontidx = (mOneFont ? FONTDEF : (a.bold ? FONTBOLD : FONTDEF));
911 bg = (a.defaultBG ? DefaultBGColorIdx : a.bg);
912 fg = (a.defaultFG ? DefaultFGColorIdx : a.fg);
913 if (a.defaultFG) {
914 if (a.bold) fg = BoldFGIdx;
915 else if (a.underline) fg = UnderlineFGIdx;
916 } else {
917 // bold means "intensity" too
918 if (a.bold && fg < 8) fg += 8;
920 if (a.reversed != reversed) {
921 immutable t = bg;
922 bg = fg;
923 fg = t;
927 // ////////////////////////////////////////////////////////////////////// //
928 void drawGlyphsInLine (int x, int y, usize len, bool cursor=true) nothrow @trusted @nogc {
929 if (y < 0 || y >= scrbuf.height || x >= scrbuf.width || len == 0) return;
930 if (len > int.max/1024) len = int.max/1024;
931 if (x < 0) {
932 x = -x;
933 if (x >= len) return;
934 len -= x;
935 x = 0;
937 if (x+len > scrbuf.width) {
938 len = scrbuf.width-x;
939 if (len == 0) return;
941 // now draw chars
942 int winx = x*charWidth;
943 immutable winy = y*charHeight;
944 immutable int curx = scrbuf.curX;
945 immutable int cury = scrbuf.curY;
946 immutable bool curvis = scrbuf.curVisible;
947 bool doSelection = scrbuf.lineHasSelection(y);
948 bool cursorLine = (cursor && curvis && y == cury);
949 while (len > 0) {
950 // draw cursor char if we hit it
951 if (cursorLine && x == curx) {
952 drawTermChar(curx, cury, /*asCursor:*/true);
953 ++x;
954 winx += charWidth;
955 --len;
956 continue;
958 // get first char attributes
959 ushort bg, fg, fontidx;
960 auto gss = scrbuf[x, y];
961 getAttrBGFGFont(gss.attr, tab.reversed, bg, fg, fontidx);
962 XFontSet xfontset = font[fontidx].set;
963 auto ascent = font[fontidx].ascent;
964 if (doSelection && scrbuf.isInSelection(x, y)) {
965 immutable t = fg;
966 fg = bg;
967 bg = t;
969 // now scan until something changed
970 usize bpos = 0;
971 immutable sx = x;
972 uint ex = cast(uint)(x+len);
973 if (cursor && curvis && y == cury && x < curx && ex > curx) ex = curx;
974 while (bpos < ex-sx) {
975 ushort bg1 = void, fg1 = void, fontidx1 = void;
976 auto gg = scrbuf[x, y];
977 getAttrBGFGFont(gg.attr, tab.reversed, bg1, fg1, fontidx1);
978 if (doSelection && scrbuf.isInSelection(x, y)) {
979 immutable t = fg1;
980 fg1 = bg1;
981 bg1 = t;
983 if (bg1 != bg || fg1 != fg || fontidx1 != fontidx) break;
984 mDrawBuf.ptr[bpos++] = gg.ch;
985 //ln.changeDirtyX(x, false);
986 scrbuf.setDirtyAt(x, y, false);
987 ++x;
989 assert(bpos <= len);
990 // draw it
991 XSetBackground(dpy, gc, getColor(bg));
992 XSetForeground(dpy, gc, getColor(fg));
993 XwcDrawImageString(dpy, cast(Drawable)win, xfontset, gc, winx, winy+ascent, mDrawBuf.ptr, cast(uint)bpos);
994 // draw underline here
995 winx += bpos*charWidth;
996 len -= bpos;
1000 // ////////////////////////////////////////////////////////////////////// //
1001 // force redrawing
1002 void drawTermPixRect (int x, int y, int wdt, int hgt) nothrow @trusted @nogc {
1003 import std.algorithm : max, min;
1004 if (wdt < 1 || hgt < 1) return;
1005 // the following can overflow; idc
1006 if (x < 0) { wdt += x; x = 0; }
1007 if (y < 0) { hgt += y; y = 0; }
1008 // check it again
1009 if (wdt < 1 || hgt < 1) return;
1010 // default tty bg color
1011 XSetForeground(dpy, gc, getColor(DefaultBGColorIdx));
1012 // fill right part of the window if necessary
1013 if (x+wdt-1 >= scrbuf.width*charWidth) {
1014 XFillRectangle(dpy, cast(Drawable)win, gc, scrbuf.width*charWidth, y, x+wdt-scrbuf.width*charWidth, hgt);
1016 // fill bottom part of the window if necessary
1017 if (y+hgt-1 >= scrbuf.height*charHeight) {
1018 XFillRectangle(dpy, cast(Drawable)win, gc, x, scrbuf.height*charWidth, wdt, y+hgt-scrbuf.height*charHeight);
1020 // should we draw glyphs?
1021 if (x >= scrbuf.width*charWidth || y >= scrbuf.height*charHeight) return;
1022 int x0 = x/charWidth;
1023 int y0 = y/charHeight;
1024 int x1 = min((x+wdt-1)/charWidth, scrbuf.width-1);
1025 int y1 = min((y+hgt-1)/charHeight, scrbuf.height-1);
1026 foreach (immutable yy; y0..y1+1) drawGlyphsInLine(x0, yy, x1-x0+1);
1029 void drawTermDirty () nothrow @trusted @nogc {
1030 //{ import core.stdc.stdio; printf("REDRAW! dc=%d\n", mScrBuf.mDirtyCount); }
1031 foreach (immutable y; 0..scrbuf.height) {
1032 if (!scrbuf.isDirtyLine(y)) continue;
1033 //{ import core.stdc.stdio; printf("REDRAW! y=%d\n", y); }
1034 uint x0 = 0;
1035 while (x0 < scrbuf.width) {
1036 // find first dirty glyph
1037 while (x0 < scrbuf.width && !scrbuf.isDirtyAt(x0, y)) ++x0;
1038 if (x0 >= scrbuf.width) break;
1039 // find last dirty glyph
1040 uint x1 = x0+1;
1041 while (x1 < scrbuf.width && !scrbuf.isDirtyAt(x1, y)) ++x1;
1042 drawGlyphsInLine(x0, y, x1-x0, /*cursor:*/true);
1043 x0 = x1;
1048 // ////////////////////////////////////////////////////////////////////// //
1049 // operates in character cells
1050 // WARNING! don't pass invalid args!
1051 void xcopytty (int dx, int dy, int sx, int sy, int wdt, int hgt) nothrow @trusted @nogc {
1052 if (wdt < 1 || hgt < 1 || (sx == dx && sy == dy)) return;
1053 dx *= charWidth;
1054 dy *= charHeight;
1055 sx *= charWidth;
1056 sy *= charHeight;
1057 wdt *= charWidth;
1058 hgt *= charHeight;
1059 XCopyArea(dpy, cast(Drawable)win, cast(Drawable)win, gc,
1060 // src
1061 sx, sy, wdt, hgt,
1062 // dst
1063 dx, dy);
1066 void undrawCursor () nothrow @trusted @nogc {
1067 if (!tab.prevcurVis) return;
1068 drawTermChar(tab.prevcurX, tab.prevcurY, /*asCursor:*/false);
1071 void drawCursor () nothrow @trusted @nogc {
1072 if (!tab.prevcurVis) return;
1073 drawTermChar(tab.prevcurX, tab.prevcurY, /*asCursor:*/true);
1076 // ////////////////////////////////////////////////////////////////////// //
1077 void doneSelection () {
1078 if (mDoSelection) {
1079 mDoSelection = false;
1080 mCurSelection = scrbuf.getSelectionText();
1081 if (mCurSelection.length == 0) mCurSelection = null;
1082 // own selection
1083 XSetSelectionOwner(dpy, XA_PRIMARY, win, CurrentTime);
1084 XSetSelectionOwner(dpy, xaClipboard, win, CurrentTime);
1085 XFlush(dpy);
1087 scrbuf.doneSelection();
1090 void xdoMouseReport (ref XEvent e) {
1091 uint x = e.xbutton.x/charWidth;
1092 uint y = e.xbutton.y/charHeight;
1093 e.xbutton.state &= (Mod1Mask|Mod4Mask|ControlMask|ShiftMask);
1095 if (mDoSelection) {
1096 if (x >= scrbuf.width) x = scrbuf.width-1;
1097 if (y >= scrbuf.height) y = scrbuf.height-1;
1098 mSelLastX = x;
1099 mSelLastY = y;
1100 scrbuf.selectionChanged(x, y);
1101 if (e.xbutton.type == ButtonRelease && e.xbutton.button == 1) doneSelection();
1102 return;
1105 if (e.xbutton.state == ShiftMask && e.xbutton.type == ButtonPress && e.xbutton.button == 1) {
1106 // selection
1107 if (x >= scrbuf.width) x = scrbuf.width-1;
1108 if (y >= scrbuf.height) y = scrbuf.height-1;
1109 mSelLastX = x;
1110 mSelLastY = y;
1111 scrbuf.resetSelection();
1112 scrbuf.selectionChanged(x, y);
1113 mDoSelection = true;
1114 return;
1117 auto button = cast(ubyte)(e.xbutton.button-Button1+1);
1118 uint mods;
1119 VT100Emu.MouseEvent event;
1121 switch (e.xbutton.type) {
1122 case MotionNotify: event = VT100Emu.MouseEvent.Motion; break;
1123 case ButtonPress: event = VT100Emu.MouseEvent.Down; break;
1124 case ButtonRelease: event = VT100Emu.MouseEvent.Up; break;
1125 default: return;
1128 mods =
1129 (e.xbutton.state&ShiftMask ? VT100Emu.MouseMods.Shift : 0) |
1130 (e.xbutton.state&Mod1Mask ? VT100Emu.MouseMods.Meta : 0) |
1131 (e.xbutton.state&ControlMask ? VT100Emu.MouseMods.Ctrl : 0) |
1132 (e.xbutton.state&Mod4Mask ? VT100Emu.MouseMods.Hyper : 0);
1134 scrbuf.doMouseReport(x, y, event, button, mods);
1137 void processResize (XConfigureEvent *e) {
1138 import std.algorithm : max, min;
1139 if (e.width < 1 || e.height < 1) return;
1140 winWidth = e.width;
1141 winHeight = e.height;
1142 int cols = max(MinBufferWidth, min(MaxBufferWidth, e.width/charWidth));
1143 int rows = max(MinBufferHeight, min(MaxBufferHeight, (e.height-charHeight)/charHeight));
1144 if (cols == scrbuf.width && rows == scrbuf.height) return;
1145 // do terminal resize
1146 tab.onResize(cols, rows);
1149 void selRequest (Atom which) {
1150 if (XGetSelectionOwner(dpy, which) != None) {
1151 XConvertSelection(dpy, which, xaTargets, xaVTSelection, win, CurrentTime);
1155 void preparePaste (XSelectionEvent* ev) {
1156 import core.stdc.config;
1157 c_ulong nitems, ofs, rem;
1158 int format;
1159 ubyte* data;
1160 Atom type;
1161 bool isutf8;
1162 int wasbrk = 0;
1163 ubyte[4] ucbuf;
1164 usize ucbufused;
1165 if (ev.property != xaVTSelection) return;
1166 if (ev.target == xaUTF8) {
1167 isutf8 = true;
1168 } else if (ev.target == XA_STRING) {
1169 isutf8 = false;
1170 } else if (ev.target == xaTargets) {
1171 // list of targets arrived, select appropriate one
1172 Atom rqtype = cast(Atom)None;
1173 Atom* targ;
1174 if (XGetWindowProperty(dpy, win, ev.property, 0, 65536, False, XA_ATOM, &type, &format, &nitems, &rem, &data)) {
1175 rqtype = XA_STRING;
1176 } else {
1177 // prefer UTF-8
1178 for (targ = cast(Atom*)data; nitems > 0; --nitems, ++targ) {
1179 if (*targ == xaUTF8) rqtype = xaUTF8;
1180 else if (*targ == XA_STRING && rqtype == None) rqtype = XA_STRING;
1182 XFree(data);
1184 if (rqtype != None) XConvertSelection(dpy, ev.selection, rqtype, xaVTSelection, win, CurrentTime);
1185 return;
1186 } else {
1187 { import core.stdc.stdio; auto aname = atomName(ev.target); stderr.fprintf("unknown selection comes: '%.*s'", cast(uint)aname.length, aname.ptr); }
1188 return;
1190 // got it
1191 enum BUFSIZ = 8192;
1192 ofs = 0;
1193 enum doconv = false; //(isutf8 ? !curterm.isUTF8 : curterm.isUTF8);
1194 //{ import iv.writer; writeln("isutf8: ", isutf8, "; doconv: ", doconv); }
1195 bool wasbrp = false;
1196 /+!!!
1197 if (curterm.bracketPaste && !wasbrp) {
1198 wasbrp = true;
1199 curterm.putData("\x1b[200~");
1202 do {
1203 int blen;
1204 //char *str;
1205 if (XGetWindowProperty(dpy, win, ev.property, ofs, BUFSIZ/4, False, AnyPropertyType, &type, &format, &nitems, &rem, &data)) {
1206 //fprintf(stderr, "Clipboard allocation failed\n");
1207 break;
1209 blen = cast(int)(nitems*format/8);
1210 if (blen) {
1211 //{ import iv.writer; writeln("data: [", (cast(const(char)*)data)[0..blen], "]"); }
1212 static if (doconv) {
1213 // we must convert data first
1214 if (isutf8) {
1215 //{ import iv.writer; writeln("utf->koi: data: [", (cast(const(char)*)data)[0..blen], "]"); }
1216 // we receiving utf8 text which must be converted to koi
1217 usize pos = 0;
1218 while (pos < blen) {
1219 if (ucbufused) {
1220 // we have partial utf char, try to complete it
1221 wchar wch;
1222 //{ import iv.writer; writefln!"utf continues (0x%02X); valid cont: %s"(data[pos], isValidUtf8Cont(cast(char)data[pos])); }
1223 if (isValidUtf8Cont(cast(char)data[pos])) {
1224 ucbuf[ucbufused++] = cast(char)data[pos++];
1225 if (utf8Decode(&wch, ucbuf[0..ucbufused])) {
1226 // valid UTF
1227 //{ import iv.writer; writeln("good utf, len=", ucbufused); }
1228 char[1] ch = uni2koi(wch);
1229 curterm.putData(ch);
1230 ucbufused = 0;
1231 } else if (ucbufused == ucbuf.length) {
1232 // invalid UTF
1233 curterm.putData(cast(char[])ucbuf[]);
1234 ucbufused = 0;
1236 continue;
1237 } else {
1238 // this is not valid UTF continuation char, flush buffer
1239 curterm.putData(cast(char[])ucbuf[0..ucbufused]);
1240 ucbufused = 0;
1243 assert(ucbufused == 0);
1244 usize end = pos;
1245 while (end < blen && !isValidUtf8Start(data[end])) ++end;
1246 if (end > pos) {
1247 curterm.putData((cast(const(char)*)data)[pos..end]);
1248 pos = end;
1249 if (pos >= blen) break;
1251 //{ import iv.writer; writeln("utf start at ", pos); }
1252 // this is valid UTF start char
1253 ucbuf[ucbufused++] = data[pos++];
1255 } else {
1256 // we receiving non-utf8 text which must be converted to utf
1257 usize pos = 0;
1258 while (pos < blen) {
1259 usize end = pos;
1260 while (end < blen && data[end] < 0x80) ++end;
1261 if (end > pos) {
1262 curterm.putData((cast(const(char)*)data)[pos..end]);
1263 pos = end;
1264 continue;
1266 // got koi char
1267 wchar ch = koi2uni(cast(char)data[pos++]);
1268 curterm.putData(utf8Encode(ucbuf[], ch));
1271 } else {
1272 //!!!curterm.putData((cast(const(char)*)data)[0..blen]);
1275 XFree(data);
1276 // number of 32-bit chunks returned
1277 ofs += nitems*format/32;
1278 } while (rem > 0);
1279 //!!!if (ucbufused > 0) curterm.putData(cast(char[])ucbuf[0..ucbufused]);
1280 //{ import iv.writer; writeln("no more data!"); }
1281 //!!!if (wasbrp) curterm.putData("\x1b[201~");
1284 void prepareCopy (XSelectionRequestEvent* ev) {
1285 XSelectionEvent xev;
1286 string text = mCurSelection;
1287 if (text.length == 0) text = ""; // let it point to something
1288 xev.type = SelectionNotify;
1289 xev.requestor = ev.requestor;
1290 xev.selection = ev.selection;
1291 xev.target = ev.target;
1292 xev.time = ev.time;
1293 // reject
1294 xev.property = cast(Atom)None;
1295 if (ev.target == xaTargets) {
1296 // respond with the supported type
1297 Atom[3] tlist = [xaUTF8, XA_STRING, xaTargets];
1298 XChangeProperty(ev.display, ev.requestor, ev.property, XA_ATOM, 32, PropModeReplace, cast(ubyte*)tlist.ptr, 3);
1299 xev.property = ev.property;
1300 } else if (ev.target == xaUTF8 || ev.target == XA_STRING) {
1301 // maybe i should convert from utf for XA_STRING, but i don't care
1302 XChangeProperty(ev.display, ev.requestor, ev.property, ev.target, 8, PropModeReplace,
1303 cast(ubyte*)text.ptr, cast(uint)text.length);
1304 xev.property = ev.property;
1306 /* all done, send a notification to the listener */
1307 if (!XSendEvent(ev.display, ev.requestor, True, 0, cast(XEvent*)&xev)) {
1308 //fprintf(stderr, "Error sending SelectionNotify event\n");
1312 void clearSelection (XSelectionClearEvent* ev) {
1313 // do nothing
1316 bool processXEvents () {
1317 void keypressEvent (ref XKeyEvent e) {
1318 // blank pointer on any keyboard event
1319 if (OptPtrBlankTime > 0) blankPointer();
1320 KeySym ksym = NoSymbol;
1321 Status status;
1322 //immutable meta = ((e.state&Mod1Mask) != 0);
1323 //immutable shift = ((e.state&ShiftMask) != 0);
1324 dchar ch;
1325 auto len = XwcLookupString(xic, &e, &ch, 1, &ksym, &status);
1326 // if "alt" (aka "meta") is pressed, get keysym name with ignored language switches
1327 if (e.state&(Mod1Mask|ControlMask)) ksym = XLookupKeysym(&e, 0);
1328 if (len != 1) ch = 0;
1329 // leave only known mods
1330 //e.state &= ~Mod2Mask; // numlock
1331 e.state &= (Mod1Mask|Mod4Mask|ControlMask|ShiftMask);
1332 // paste selections
1333 if (ksym == XK_Insert && (e.state&(Mod1Mask|ShiftMask)) != 0 && (e.state&~(Mod1Mask|ShiftMask)) == 0) {
1334 // shift: primary
1335 // alt: clipboard
1336 // alt+shift: secondary
1337 if (e.state == ShiftMask) selRequest(XA_PRIMARY);
1338 else if (e.state == Mod1Mask) selRequest(xaClipboard);
1339 else selRequest(XA_SECONDARY);
1340 return;
1342 tab.keypressEvent(ch, ksym, X11ModState(e.state));
1345 // event processing loop
1346 bool quit = false;
1347 XEvent event = void;
1348 while (!quit && XPending(dpy)) {
1349 XNextEvent(dpy, &event);
1350 if (XFilterEvent(&event, cast(Window)0)) continue;
1351 if (event.type == KeymapNotify) {
1352 XRefreshKeyboardMapping(&event.xmapping);
1353 continue;
1355 if (event.xany.window != win) {
1356 //{ import iv.writer; writeln("CRAP!"); }
1357 continue;
1360 switch (event.type) {
1361 case ClientMessage:
1362 if (event.xclient.message_type == xaWMProtocols) {
1363 if (cast(Atom)event.xclient.data.l[0] == xaWMDeleteWindow) {
1364 quit = true;
1366 } else if (event.xclient.message_type == xaTerminalMessage) {
1367 if (event.xclient.data.l[0] == WinMsgDirty) {
1368 drawTermDirty();
1371 break;
1372 case KeyPress:
1373 keypressEvent(event.xkey);
1374 break;
1375 case Expose:
1376 if (!mForceRedraw) {
1377 auto ee = cast(XExposeEvent*)&event;
1378 drawTermPixRect(ee.x, ee.y, ee.width, ee.height);
1379 // fix last redraw time if no dirty areas left
1380 if (!scrbuf.isDirty) resetRedrawInfo();
1382 break;
1383 case GraphicsExpose:
1384 if (!mForceRedraw) {
1385 drawTermPixRect(event.xgraphicsexpose.x, event.xgraphicsexpose.y, event.xgraphicsexpose.width, event.xgraphicsexpose.height);
1386 // fix last redraw time if no dirty areas left
1387 if (!scrbuf.isDirty) resetRedrawInfo();
1389 //event.xgraphicsexpose.count
1390 break;
1391 case VisibilityNotify:
1392 if (event.xvisibility.state == VisibilityFullyObscured) {
1393 mWinState &= ~wsVisible;
1394 } else {
1395 mWinState |= wsVisible;
1397 break;
1398 case FocusIn:
1399 XSetICFocus(xic);
1400 if (!(mWinState&wsFocused)) {
1401 mWinState |= wsFocused;
1402 scrbuf.onBlurFocus(true);
1404 break;
1405 case FocusOut:
1406 doneSelection();
1407 XUnsetICFocus(xic);
1408 if (mWinState&wsFocused) {
1409 mWinState &= ~wsFocused;
1410 scrbuf.onBlurFocus(false);
1412 break;
1413 case MotionNotify:
1414 case ButtonPress:
1415 case ButtonRelease:
1416 unblankPointer();
1417 xdoMouseReport(event);
1418 break;
1419 case SelectionNotify:
1420 // we got our requested selection, and should paste it
1421 preparePaste(&event.xselection);
1422 break;
1423 case SelectionRequest:
1424 prepareCopy(&event.xselectionrequest);
1425 break;
1426 case SelectionClear:
1427 clearSelection(&event.xselectionclear);
1428 break;
1429 case ConfigureNotify:
1430 processResize(&event.xconfigure);
1431 break;
1432 default:
1435 return quit;
1438 // ////////////////////////////////////////////////////////////////////// //
1439 void resetRedrawInfo () nothrow @trusted @nogc {
1440 mLastDrawTime = MonoTime.currTime;
1441 mForceRedraw = false;
1442 mForceDirtyRedraw = false;
1445 void fullRedraw () nothrow @trusted @nogc {
1446 drawTermPixRect(0, 0, winWidth, winHeight);
1447 resetRedrawInfo();
1450 // ////////////////////////////////////////////////////////////////////// //
1451 void mainLoop () {
1452 import std.algorithm : max;
1453 import core.sys.posix.sys.select;
1454 import core.sys.posix.sys.time : timeval;
1455 fd_set rds, wrs;
1456 int maxfd;
1457 bool mPrevFocused = false;
1459 void addRFD (int fd) {
1460 if (fd >= 0) {
1461 if (fd+1 > maxfd) maxfd = fd+1;
1462 FD_SET(fd, &rds);
1466 void addWFD (int fd) {
1467 if (fd >= 0) {
1468 if (fd+1 > maxfd) maxfd = fd+1;
1469 FD_SET(fd, &wrs);
1473 bool timeToHidePointer () {
1474 return (OptPtrBlankTime > 0 ? (MonoTime.currTime-mLastInputTime).total!"msecs" >= OptPtrBlankTime : false);
1477 // <0: no blinking
1478 int msUntilNextCursorBlink () {
1479 if (!isVisible) return -1;
1480 if (isFocused != mPrevFocused) return 0; // must update cursor NOW!
1481 if (!scrbuf.curVisible && !tab.prevcurVis) return -1; // nothing was changed
1482 auto now = MonoTime.currTime;
1483 if (isFocused) {
1484 // blinking
1485 auto ntt = (now-mCurLastPhaseChange).total!"msecs";
1486 if (ntt >= OptPtrBlinkTime) return 0; // NOW!
1487 if (scrbuf.curVisible != tab.prevcurVis || scrbuf.curX != tab.prevcurX || scrbuf.curY != tab.prevcurY) {
1488 ntt = OptPtrBlinkTime-ntt;
1489 // check last move time
1490 auto lmt = (now-mCurLastMove).total!"msecs";
1491 if (lmt >= 50) return 0; // NOW! -- cursor state changed
1492 if (50-lmt < ntt) return cast(int)(50-lmt);
1494 return cast(int)ntt;
1495 } else {
1496 // not blinking, check last move time
1497 if (scrbuf.curVisible == tab.prevcurVis && scrbuf.curX == tab.prevcurX && scrbuf.curY == tab.prevcurY) return -1; // never
1498 auto lmt = (now-mCurLastMove).total!"msecs";
1499 if (lmt >= 150) return 0; // NOW! -- cursor state changed
1500 return cast(int)(150-lmt);
1504 // <0: already hidden, or not required
1505 int msUntilPointerHiding () {
1506 if (!isVisible || !isFocused || !mPointerVisible || OptPtrBlankTime < 1) return -1;
1507 auto now = MonoTime.currTime;
1508 auto ntt = (now-mLastInputTime).total!"msecs";
1509 if (ntt >= OptPtrBlankTime) return 0; // NOW!
1510 return cast(int)(OptPtrBlankTime-ntt);
1513 @property int redrawDelay () nothrow @trusted @nogc { return (isFocused ? RedrawRateActive : RedrawRateInactive); }
1515 // <0: no redraw required
1516 int msUntilRedraw () {
1517 if (!isVisible || !scrbuf.isDirty) return -1;
1518 auto now = MonoTime.currTime;
1519 auto ntt = (now-mLastDrawTime).total!"msecs";
1520 if (ntt >= redrawDelay) return 0; // NOW!
1521 return cast(int)(redrawDelay-ntt);
1524 void doCursorBlink () {
1525 bool doCursorRedraw = false;
1526 auto now = MonoTime.currTime;
1527 if (scrbuf.curVisible != tab.prevcurVis || scrbuf.curX != tab.prevcurX || scrbuf.curY != tab.prevcurY) {
1528 if (tab.prevcurVis) {
1529 if (!scrbuf.isDirty) {
1530 drawTermChar(tab.prevcurX, tab.prevcurY, /*asCursor:*/false);
1531 } else {
1532 scrbuf.setDirtyAt(tab.prevcurX, tab.prevcurY, true);
1535 tab.prevcurVis = scrbuf.curVisible;
1536 if (tab.prevcurVis) doCursorRedraw = true;
1537 tab.prevcurX = scrbuf.curX;
1538 tab.prevcurY = scrbuf.curY;
1539 mCurLastMove = now;
1541 if (isFocused) {
1542 if (!mPrevFocused) {
1543 // window was not focused, force cursor redraw
1544 doCursorRedraw = true;
1545 mCurPhase = 0;
1546 } else if ((now-mCurLastPhaseChange).total!"msecs" >= OptPtrBlinkTime) {
1547 // window was focused and cursor must blink
1548 mCurPhase ^= 1;
1549 doCursorRedraw = true;
1551 } else {
1552 // not focused
1553 doCursorRedraw = (scrbuf.curVisible && (mPrevFocused || mCurPhase != 0));
1554 mCurPhase = 0;
1556 if (scrbuf.curVisible && doCursorRedraw) {
1557 if (isFocused == mPrevFocused && scrbuf.isDirty || msUntilRedraw == 0) {
1558 // mark current cursor cell as dirty, redraw will follow
1559 scrbuf.setDirtyAt(scrbuf.curX, scrbuf.curY, true);
1560 } else {
1561 // immediate redraw
1562 drawTermChar(scrbuf.curX, scrbuf.curY, /*asCursor:*/true);
1563 XFlush(dpy);
1566 if (doCursorRedraw) mCurLastPhaseChange = now;
1567 mPrevFocused = isFocused;
1570 // return true if we should exit
1571 bool doXEvents () {
1572 immutable ovs = isVisible;
1573 if (processXEvents()) return true;
1574 if (ovs != isVisible) {
1575 // visibility changed
1576 if (!isVisible) {
1577 // becomes hidden, cancel any pending redraw
1578 mForceRedraw = false;
1579 } else {
1580 // becomes visible, do full redraw
1581 // don't need that, as Expose will redraw damaged areas
1583 mForceRedraw = true;
1587 return false;
1590 // find max possible wait interval (0: don't wait at all; -1: forever)
1591 int calcMaxWaitTime () {
1592 if (isVisible) {
1593 if (isFocused != mPrevFocused) return 0; // now!
1594 int ntt = msUntilNextCursorBlink();
1595 //{ import core.stdc.stdio; printf("until blink: %d\n", ntt); }
1596 if (ntt == 0) return 0; // now!
1598 int ntt1 = msUntilPointerHiding();
1599 //{ import core.stdc.stdio; printf("until hide: %d\n", ntt1); }
1600 if (ntt1 == 0) return 0; // now!
1601 if (ntt < 0 || (ntt1 >= 0 && ntt > ntt1)) ntt = ntt1;
1604 int ntt1 = msUntilRedraw();
1605 //{ import core.stdc.stdio; printf("until redraw: %d\n", ntt1); }
1606 if (ntt1 == 0) return 0; // now!
1607 if (ntt < 0 || (ntt1 >= 0 && ntt > ntt1)) ntt = ntt1;
1609 //{ import core.stdc.stdio; printf("RES: %d\n", ntt); }
1610 return (ntt > 0 ? ntt+1 : ntt); // add one msec to really trigger the event
1611 } else {
1612 // window is invisible, we can ignore any timed events
1613 return -1; // forever
1617 mCurLastPhaseChange = MonoTime.currTime;
1618 mCurLastMove = mCurLastPhaseChange;
1619 mCurPhase = 0;
1620 mPrevFocused = isFocused;
1621 if (tab.checkFixProcess()) changeTitle(tab.mLastProcess[0..tab.mLastProcessLength]);
1622 fullRedraw();
1623 XFlush(dpy);
1624 mPointerVisible = false;
1625 unblankPointer();
1626 int xfd = XConnectionNumber(dpy);
1627 main_loop: for (;;) {
1628 immutable waitTimeMS = calcMaxWaitTime();
1629 maxfd = 0;
1630 FD_ZERO(&rds);
1631 FD_ZERO(&wrs);
1632 // setup misc sockets
1633 addRFD(xfd);
1634 addRFD(csigfd);
1635 foreach (immutable int fd; tab.appbuf.getReadFDs) if (fd >= 0) addRFD(fd);
1636 foreach (immutable int fd; tab.appbuf.getWriteFDs) if (fd >= 0) addWFD(fd);
1637 int sres = 0;
1638 if (waitTimeMS >= 0) {
1639 timeval tv;
1640 tv.tv_sec = waitTimeMS/1000;
1641 tv.tv_usec = (waitTimeMS%1000)*1000;
1642 //{ import core.stdc.stdio; printf("wait(0:%d)\n", waitTimeMS); }
1643 sres = select(maxfd, &rds, &wrs, null, &tv);
1644 //{ import core.stdc.stdio; printf("wait(1:%d)\n", waitTimeMS); }
1645 } else {
1646 // forever
1647 //{ import core.stdc.stdio; printf("forever(0)\n"); }
1648 sres = select(maxfd, &rds, &wrs, null, null);
1649 //{ import core.stdc.stdio; printf("forever(1)\n"); }
1651 if (sres < 0) {
1652 import core.stdc.errno;
1653 if (errno == EINTR) continue;
1654 die("select() error");
1656 if (sres == 0) {
1657 // timeout
1658 //continue;
1660 // check if terminal is dead
1661 if (csigfd >= 0 && FD_ISSET(csigfd, &rds)) processDeadChildren();
1662 // blink cursor
1663 doCursorBlink();
1664 // process X events
1665 if (doXEvents()) break;
1666 // send generated data to app; this can't trigger updates
1667 // also, read data from app and write it to terminal
1668 foreach (immutable int fd; tab.appbuf.getWriteFDs) if (fd >= 0 && FD_ISSET(fd, &wrs)) tab.appbuf.canWriteTo(fd);
1669 foreach (immutable int fd; tab.appbuf.getReadFDs) if (fd >= 0 && FD_ISSET(fd, &rds)) tab.appbuf.canReadFrom(fd);
1670 // update window
1671 auto nowTime = MonoTime.currTime;
1672 bool doFlush = false;
1673 // do immediate full redraw only if we are focused
1674 if (mForceRedraw && !isFocused) {
1675 mForceRedraw = false;
1676 scrbuf.setFullDirty();
1678 if (mForceRedraw) {
1679 //{ import core.stdc.stdio; stderr.fprintf("FORCE REDRAW!\n"); }
1680 fullRedraw();
1681 doFlush = true;
1682 } else {
1683 if (mForceDirtyRedraw || msUntilRedraw == 0) {
1684 if (scrbuf.isDirty) {
1685 //{ import core.stdc.stdio; stderr.fprintf("DIRTY REDRAW!\n"); }
1686 drawTermDirty();
1687 resetRedrawInfo();
1688 doFlush = true;
1689 } else {
1690 resetRedrawInfo();
1694 if (timeToHidePointer) blankPointer();
1695 // fix title
1696 if ((nowTime-mLastTitleChangeTime).total!"msecs" >= 500) {
1697 if (tab.checkFixProcess()) {
1698 changeTitle(tab.mLastProcess[0..tab.mLastProcessLength]);
1699 doFlush = false; // flush already done
1701 mLastTitleChangeTime = nowTime;
1703 if (doFlush) XFlush(dpy);
1704 // exit on dead pty
1705 if (!tab.appbuf.isPtyActive) break;
1711 void usage () {
1712 write("usage: sampleterm [runcmd...]\n");
1713 throw new ExitException();
1717 string[] processCLIArgs (string[] args) {
1718 string[] res = [args[0]];
1719 bool noMoreArgs = false;
1720 for (usize aidx = 1; aidx < args.length; ++aidx) {
1721 auto arg = args[aidx];
1722 if (noMoreArgs || arg.length == 0 || arg[0] != '-') {
1723 res ~= args[aidx];
1724 continue;
1726 if (arg == "--") { noMoreArgs = true; continue; }
1727 stderr.writeln("FATAL: unknown option: ", arg.quote);
1728 throw new ExitError();
1730 return res;
1734 int main (string[] args) {
1735 try {
1736 args = processCLIArgs(args);
1737 auto xw = new DiWindow(80, 24);
1738 xw.changeTitle("DiTTY sample terminal");
1739 xw.appbuf.execPty(args[1..$]);
1740 xw.mainLoop();
1741 xw.close();
1742 return 0;
1743 } catch (ExitException e) {
1744 if (cast(ExitError)e) return -1;
1745 return 0;