better quoting
[bioacid.git] / tklog.d
blobc1eed716ccab1567050e3e2103035eb9219bc6e8
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 tklog is aliced;
18 import std.datetime;
20 import arsd.color;
21 import arsd.image;
22 import arsd.simpledisplay;
24 import iv.cmdcon;
25 import iv.cmdcon.gl;
26 import iv.nanovega;
27 import iv.nanovega.blendish;
28 import iv.nanovega.textlayouter;
29 import iv.strex;
30 import iv.tox;
31 import iv.unarray;
32 import iv.utfutil;
33 import iv.vfs.io;
35 import accdb;
36 import accobj;
37 import fonts;
38 import popups;
39 import icondata;
40 import notifyicon;
41 import toxproto;
43 import tkmain;
46 // ////////////////////////////////////////////////////////////////////////// //
47 alias LayTextClass = LayTextD;
50 // ////////////////////////////////////////////////////////////////////////// //
51 __gshared LayTextClass lay;
52 __gshared int layWinHeight = 0;
53 __gshared int layOffset = 0; // from bottom
55 static struct LayUrl {
56 string url;
57 union {
58 usize refcount;
59 usize nextFree; // index+1
62 @property bool isFree () const nothrow @safe @nogc => (url.length == 0);
65 private __gshared usize[string] layUrlIndex;
66 private __gshared usize layUrlFreeHead = 0;
67 private __gshared LayUrl[] layUrlArray;
70 private usize allocUrl (string url) {
71 usize res = layUrlFreeHead;
72 if (res) {
73 --res; // normalize
74 layUrlFreeHead = layUrlArray[res].nextFree;
75 } else {
76 res = layUrlArray.length;
77 layUrlArray.length += 1;
78 layUrlArray.assumeSafeAppend;
80 layUrlArray[res].url = url;
81 layUrlArray[res].refcount = 1;
82 layUrlIndex[url] = res;
83 return res;
87 private void freeUrl (usize idx) {
88 assert(idx < layUrlArray.length);
89 assert(!layUrlArray[idx].isFree);
90 if (--layUrlArray[idx].refcount) return;
91 //conwriteln("FREE URL idx=", idx, "; url=[", layUrlArray[idx].url, "]");
92 layUrlIndex.remove(layUrlArray[idx].url);
93 layUrlArray[idx].url = null;
94 layUrlArray[idx].nextFree = layUrlFreeHead;
95 layUrlFreeHead = idx+1;
99 string findUrlByWordIndex (uint widx) {
100 auto w = lay.wordByIndex(widx);
101 if (w is null) return null;
102 usize pos = w.udata;
103 if (pos < 1 || pos > layUrlArray.length) return null;
104 //conwriteln(" pos=", pos, "; url=[", layUrlArray[pos-1].url, "]");
105 --pos;
106 return layUrlArray[pos].url;
110 private void appendUrl (uint widx, string url) {
111 auto w = lay.wordByIndex(widx);
112 if (w is null) return;
113 assert(w.udata == 0);
114 if (url.length == 0) return;
115 auto ip = url in layUrlIndex;
116 if (ip) {
117 assert(!layUrlArray[*ip].isFree);
118 ++layUrlArray[*ip].refcount;
119 w.udata = (*ip)+1;
120 //conwriteln("OLD URL widx=", widx, "; udata=", w.udata, "; rc=", layUrlArray[*ip].refcount, "; url=[", layUrlArray[*ip].url, "]");
121 } else {
122 usize pos = allocUrl(url);
123 w.udata = pos+1;
124 //conwriteln("NEW URL widx=", widx, "; udata=", w.udata, "; rc=", layUrlArray[pos].refcount, "; url=[", layUrlArray[pos].url, "]");
129 private void urlWordsRemoved (uint widx, int count) {
130 if (count < 1) return;
131 foreach (uint wpos; widx..widx+count) {
132 auto w = lay.wordByIndex(wpos);
133 if (w is null) continue;
134 usize pos = w.udata;
135 if (!pos) continue;
136 --pos;
137 assert(pos < layUrlArray.length);
138 assert(!layUrlArray[pos].isFree);
139 assert(layUrlArray[pos].refcount);
140 //conwriteln("FREE WORD URL widx=", wpos, "; udata=", w.udata, "; rc=", layUrlArray[pos].refcount, "; url=[", layUrlArray[pos].url, "]");
141 freeUrl(pos);
146 private void urlWipeAll () {
147 layUrlArray.unsafeArrayClear();
148 layUrlIndex.clear();
149 layUrlFreeHead = 0;
153 // ////////////////////////////////////////////////////////////////////////// //
154 class MessageStart : LayObject {
155 long msgid; // >0: outgoing, unacked yet
157 this (long aid=-1) nothrow @safe @nogc { msgid = aid; }
159 override int width () => 0;
160 override int spacewidth () => 0;
161 override int height () => 0;
162 override int ascent () => 0;
163 override int descent () => 0;
164 override bool canbreak () => true;
165 override bool spaced () => false;
166 // y is at baseline
167 override void draw (NVGContext ctx, float x, float y) {}
171 class MessageDividerStart : LayObject {
172 long msgid; // >0: outgoing, unacked yet
174 this (long aid=-1) nothrow @safe @nogc { msgid = aid; }
176 override int width () => 0;
177 override int spacewidth () => 0;
178 override int height () => 0;
179 override int ascent () => 0;
180 override int descent () => 0;
181 override bool canbreak () => true;
182 override bool spaced () => false;
183 // y is at baseline
184 override void draw (NVGContext ctx, float x, float y) {}
188 class MessageOutMark : LayObject {
189 long msgid; // >=0: outgoing, unacked yet (0: sent in offline mode)
190 TextDigest digest; // text digest
191 SysTime time;
193 this (long aid, SysTime atime, const(void)[] atext) nothrow @safe @nogc { msgid = aid; time = atime; digest = textDigest(atext); }
195 override int width () => kittyOut.width;
196 override int spacewidth () => 4;
197 override int height () => kittyOut.height;
198 override int ascent () => 0;
199 override int descent () => 0;
200 override bool canbreak () => true;
201 override bool spaced () => false;
202 // y is at baseline
203 override void draw (NVGContext ctx, float x, float y) {
204 if (msgid >= 0) {
205 nvg.save();
206 scope(exit) nvg.restore;
207 nvg.newPath();
208 // +3 is a hack
209 nvg.rect(x, y-height+3, width, height);
210 nvg.fillPaint(nvg.imagePattern(x, y-height+3, width, height, 0, kittyOut));
211 nvg.fill();
215 final bool isOurMark (const(void)[] atext, SysTime atime) nothrow @trusted {
216 pragma(inline, true);
217 return (atime == time && digest[] == textDigest(atext));
220 final bool isOurMark (long aid, const(void)[] atext, SysTime atime) nothrow @trusted {
221 pragma(inline, true);
222 return (aid > 0 ? (msgid == aid) : (atime == time && digest[] == textDigest(atext)));
227 // ////////////////////////////////////////////////////////////////////////// //
228 void logFixAckMessageId (long oldid, long newid, const(void)[] text, SysTime time) {
229 if (oldid == newid) return; // nothing to do
230 foreach (immutable uint widx; 0..lay.wordCount) {
231 auto w = lay.wordByIndex(widx);
232 int oidx = w.objectIdx;
233 if (oidx >= 0) {
234 if (auto maw = cast(MessageOutMark)lay.objectAtIndex(oidx)) {
235 if (maw.isOurMark(oldid, text, time)) {
236 maw.msgid = newid;
237 glconPostScreenRepaint(); // redraw
244 void logResetAckMessageIds () {
245 bool doRefresh = false;
246 foreach (immutable uint widx; 0..lay.wordCount) {
247 auto w = lay.wordByIndex(widx);
248 int oidx = w.objectIdx;
249 if (oidx >= 0) {
250 if (auto maw = cast(MessageOutMark)lay.objectAtIndex(oidx)) {
251 if (maw.msgid > 0) {
252 maw.msgid = 0;
253 doRefresh = true;
258 if (doRefresh) glconPostScreenRepaint(); // redraw
261 void ackLogMessage (long msgid) {
262 if (msgid <= 0) return;
263 foreach (immutable uint widx; 0..lay.wordCount) {
264 auto w = lay.wordByIndex(widx);
265 int oidx = w.objectIdx;
266 if (oidx >= 0) {
267 if (auto maw = cast(MessageOutMark)lay.objectAtIndex(oidx)) {
268 if (maw.msgid == msgid) {
269 maw.msgid = -1; // reset mark
270 glconPostScreenRepaint(); // redraw
278 // coordinates are adjusted so (0, 0) points to logical layouter top-left
280 bool logCheckCancelMark (int mx, int my) {
281 bool removeFromResendQueue (long msgid, in ref TextDigest digest, SysTime time) {
285 // ////////////////////////////////////////////////////////////////////////// //
286 void wipeLog () {
287 lay.wipeAll(true); // clear log, but delete objects
288 urlWipeAll();
289 layOffset = 0;
293 // ////////////////////////////////////////////////////////////////////////// //
294 void addDividerLine (bool doflushgui=false) {
295 if (glconCtlWindow is null || glconCtlWindow.closed) return;
298 bool inFrame = nvg.inFrame;
299 if (!inFrame) {
300 glconCtlWindow.setAsCurrentOpenGlContext(); // make this window active
301 glViewport(0, 0, glconCtlWindow.width, glconCtlWindow.height);
302 nvg.beginFrame(glconCtlWindow.width, glconCtlWindow.height);
304 scope(exit) {
305 if (!inFrame) {
306 nvg.endFrame();
307 if (doflushgui) flushGui();
308 glconCtlWindow.releaseCurrentOpenGlContext();
312 uint widx = 0;
313 while (widx < lay.wordCount) {
314 auto w = lay.wordByIndex(widx);
315 int oidx = w.objectIdx;
316 if (oidx >= 0) {
317 if (auto maw = cast(MessageDividerStart)lay.objectAtIndex(oidx)) {
318 // this always followed by expander; remove them both
319 lay.removeWordsAt(widx, 2);
320 urlWordsRemoved(widx, 2);
321 continue;
324 ++widx;
327 lay.fontStyle.fontsize = 2;
328 lay.fontStyle.color = NVGColor.k8orange.asUint;
329 lay.fontStyle.bgcolor = NVGColor("#aa0").asUint;
330 lay.fontStyle.monospace = true;
332 lay.putObject(new MessageDividerStart());
333 lay.putExpander();
334 lay.endPara();
335 lay.finalize();
338 // redraw
339 glconPostScreenRepaint();
343 // ////////////////////////////////////////////////////////////////////////// //
344 // `ct` can be `null` for "my message" or "system message"
345 void addTextToLog (Account acc, Contact ct, LogFile.Msg.Kind kind, bool action,
346 const(char)[] msg, SysTime time, long msgid=-1, bool doflushgui=false)
348 if (glconCtlWindow is null || glconCtlWindow.closed) return;
351 bool inFrame = nvg.inFrame;
352 if (!inFrame) {
353 glconCtlWindow.setAsCurrentOpenGlContext(); // make this window active
354 nvg.beginFrame(glconCtlWindow.width, glconCtlWindow.height);
356 scope(exit) {
357 if (!inFrame) {
358 nvg.endFrame();
359 if (doflushgui) flushGui();
360 glconCtlWindow.releaseCurrentOpenGlContext();
364 // create header
366 // add "message start" mark
367 lay.putObject(new MessageStart(msgid));
369 lay.fontStyle.fontsize = 16;
370 lay.fontStyle.color = NVGColor.k8orange.asUint;
371 //lay.fontStyle.bgcolor = NVGColor("#222").asUint;
372 lay.fontStyle.bgcolor = NVGColor("#5888").asUint;
373 lay.fontStyle.monospace = true;
375 NVGColor textColor;
376 int fontSize = 20;
378 final switch (kind) {
379 case LogFile.Msg.Kind.Outgoing:
380 textColor = NVGColor.k8orange; lay.fontStyle.color = NVGColor("#c40").asUint;
381 lay.put(acc.info.nick);
382 //fontSize = 19;
383 break;
384 case LogFile.Msg.Kind.Incoming:
385 textColor = NVGColor("#aaa");
386 lay.fontStyle.color = NVGColor("#777").asUint;
387 lay.put(ct.info.nick);
388 //fontSize = 21;
389 break;
390 case LogFile.Msg.Kind.Notification:
391 textColor = NVGColor("#0c0");
392 lay.fontStyle.color = textColor.asUint;
393 lay.put("*system*");
394 //fontSize = 20;
395 break;
398 lay.fontStyle.monospace = false;
399 //lay.putHardSpace(64);
400 lay.putExpander();
401 // add "message outgoing" mark
402 if (kind == LogFile.Msg.Kind.Outgoing) {
403 //conwriteln("OG: msgid=", msgid);
404 if (msgid <= 0 && ct !is null) {
405 msgid = ct.findInResendQueue(msg, time);
406 //conwriteln(" new msgid=", msgid);
408 lay.putObject(new MessageOutMark(msgid, time, msg));
409 lay.putExpander();
412 // date
413 if (kind == LogFile.Msg.Kind.Incoming) lay.fontStyle.color = NVGColor("#888").asUint;
415 import std.datetime;
416 import std.format : format;
417 auto dt = cast(DateTime)time;
418 string tstr = "%04u/%02u/%02u".format(dt.year, dt.month, dt.day);
419 lay.put(tstr);
420 lay.putNBSP();
423 // time
424 lay.fontStyle.color = (kind == LogFile.Msg.Kind.Incoming ? NVGColor("#888").asUint : NVGColor("#666").asUint);
425 lay.fontStyle.bgcolor = NVGColor("#006").asUint;
427 import std.datetime;
428 import std.format : format;
429 auto dt = cast(DateTime)time;
430 string tstr = "%02u:%02u:%02u".format(dt.hour, dt.minute, dt.second);
431 lay.put(tstr);
432 lay.putNBSP();
434 lay.endPara();
436 // message text
437 lay.fontStyle.bgcolor = NVGColor.transparent.asUint;
438 lay.fontStyle.fontsize = fontSize;
440 // action mark
441 if (action) {
442 lay.fontStyle.color = NVGColor("#fff").asUint;
443 lay.put("/me ");
446 static bool isWordDelim (dchar ch) {
447 import std.uni : isWhite;
448 return
449 ch == lay.EndLineCh || ch == lay.EndParaCh ||
450 //ch == lay.NBSpaceCh || ch == lay.NarrowNBSpaceCh ||
451 ch == lay.SoftHyphenCh ||
452 ch <= ' ' || isWhite(ch);
455 void layPutSplitLongWords (const(char)[] str) {
456 enum MaxCharsSplit = 16;
457 Utf8Decoder dec;
458 dec.reset();
459 int lastWordLength = 0;
460 while (str.length) {
461 dchar curCh = dec.decode(cast(ubyte)str[0]);
462 str = str[1..$];
463 if (curCh > dchar.max) continue;
464 if (isWordDelim(curCh)) {
465 lastWordLength = 0;
466 } else {
467 if (++lastWordLength >= MaxCharsSplit) {
468 lay.putSoftHypen();
469 lastWordLength = 0;
472 lay.put(curCh);
476 bool wasNL = !action;
477 bool inQuote = false;
479 void setFontStyle () {
480 if (inQuote) {
481 lay.fontStyle.color = NVGColor("#ff0").asUint;
482 lay.fontStyle.fontsize = fontSize-1;
483 lay.fontStyle.italic = true;
484 } else {
485 lay.fontStyle.color = textColor.asUint;
486 lay.fontStyle.fontsize = fontSize;
487 lay.fontStyle.italic = false;
491 void checkQuote (const(char)[] str) {
492 if (wasNL) {
493 inQuote = (str[0] == '>');
494 setFontStyle();
498 void xput (const(char)[] str) {
499 while (str.length) {
500 checkQuote(str);
501 auto nl = str.indexOf('\n');
502 if (nl < 0) {
503 layPutSplitLongWords(str);
504 break;
506 if (nl > 0) {
507 layPutSplitLongWords(str[0..nl]);
509 lay.endPara();
510 wasNL = !action;
511 inQuote = false;
512 str = str[nl+1..$];
516 msg = msg.xstripright;
518 lay.fontStyle.color = textColor.asUint;
519 while (msg.length) {
520 auto nfo = urlDetect(msg);
521 if (!nfo.valid) {
522 xput(msg);
523 break;
525 // url found
526 xput(msg[0..nfo.pos]);
527 string url = msg[nfo.pos..nfo.end].idup;
528 msg = msg[nfo.end..$];
529 auto stword = lay.nextWordIndex;
530 lay.pushStyles();
531 auto c = lay.fontStyle.color;
532 scope(exit) { lay.popStyles; lay.fontStyle.color = c; }
533 lay.fontStyle.href = true;
534 lay.fontStyle.underline = true;
535 lay.fontStyle.color = NVGColor("#06f").asUint;
536 lay.put(url);
537 if (msg.length) {
538 lay.endWord(spaced:(msg[0] <= ' ' && msg[0] != '\n'));
539 } else {
540 lay.endWord(spaced:false);
542 while (stword < lay.nextWordIndex) {
543 appendUrl(stword, url);
544 ++stword;
546 setFontStyle();
549 lay.endPara();
550 lay.finalize();
552 // redraw
553 glconPostScreenRepaint();
557 void addTextToLog (Account acc, Contact ct, in ref LogFile.Msg msg, long msgid=-1, bool doflushgui=false) {
558 import iv.utfutil : utf8Encode;
559 char[] text;
560 text.reserve(4096);
561 scope(exit) delete text;
562 // decode text
563 foreach (dchar dc; msg.byDChar) {
564 char[4] buf = void;
565 auto len = utf8Encode(buf[], dc);
566 text ~= buf[0..len];
568 addTextToLog(acc, ct, msg.kind, msg.isMe, text, msg.time, msgid, doflushgui);