don't make refs too long (bug)
[knntp.git] / nntpreader.d
blob89b0846d48d6a85a338dfb8805df38331fb63edd
1 /* DigitalMars NNTP reader
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 module nntpreader is aliced;
20 import core.atomic;
21 import core.time;
22 import std.concurrency;
24 import arsd.email;
25 import arsd.htmltotext;
26 import arsd.simpledisplay;
28 import iv.bclamp;
29 import iv.encoding;
30 import iv.cmdcon;
31 import iv.cmdcongl;
32 import iv.lockfile;
33 import iv.strex;
34 import iv.utfutil;
35 import iv.vfs;
37 import egfx;
38 import nntp;
39 import editor;
40 import twitlist;
41 import group;
42 import egui;
45 // ////////////////////////////////////////////////////////////////////////// //
46 __gshared int oldMouseX, oldMouseY;
47 __gshared ubyte oldMouseButtons;
48 __gshared bool dbg_dump_keynames;
51 // ////////////////////////////////////////////////////////////////////////// //
52 __gshared Tid updateThreadId;
53 __gshared bool updateInProgress = false;
56 // ////////////////////////////////////////////////////////////////////////// //
57 enum UpThreadCommand {
58 Ping,
59 StartUpdate, // start updating now
60 Quit,
63 void updateThread (Tid ownerTid) {
64 bool doQuit = false;
65 bool doUpdates = false;
66 try {
67 while (!doQuit) {
68 receive(
69 (UpThreadCommand cmd) {
70 final switch (cmd) {
71 case UpThreadCommand.Ping: break;
72 case UpThreadCommand.StartUpdate: doUpdates = true; break;
73 case UpThreadCommand.Quit: doQuit = true; break;
77 if (doQuit) break;
78 if (doUpdates) {
79 doUpdates = false;
80 foreach (immutable gidx, Group g; groups) {
81 if (g.needUpdate) {
82 if (vbwin !is null) vbwin.postEvent(new UpdatingGroupEvent(cast(uint)gidx));
83 g.doUpdate();
84 if (vbwin !is null) vbwin.postEvent(new UpdatingGroupCompleteEvent(cast(uint)gidx));
85 //conwriteln("group '", g.mbase.groupname, "' update complete");
88 //conwriteln("all groups updated");
89 if (vbwin !is null) {
90 vbwin.postEvent(new UpdatingCompleteEvent());
91 //conwriteln("complete event sent");
95 } catch (Throwable e) {
96 // here, we are dead and fucked (the exact order doesn't matter)
97 import core.stdc.stdlib : abort;
98 import core.stdc.stdio : fprintf, stderr;
99 import core.memory : GC;
100 import core.thread : thread_suspendAll;
101 GC.disable(); // yeah
102 thread_suspendAll(); // stop right here, you criminal scum!
103 auto s = e.toString();
104 fprintf(stderr, "\n=== FATAL ===\n%.*s\n", cast(uint)s.length, s.ptr);
105 abort(); // die, you bitch!
110 // ////////////////////////////////////////////////////////////////////////// //
111 void initConsole () {
112 conRegVar!VBufScale(1, 4, "v_scale", "window scale: [1..3]");
114 conRegVar!bool("v_vsync", "sync to video refresh rate?",
115 (ConVarBase self) => vbufVSync,
116 (ConVarBase self, bool nv) {
117 if (vbufVSync != nv) {
118 vbufVSync = nv;
119 postScreenRepaint();
124 conRegFunc!(() {
125 if (groups.length == 0) return;
126 auto gidx = getActiveGroupIndex();
127 if (gidx == 0) return;
128 if (gidx >= 0) groups[gidx].deactivate();
129 gidx = (gidx < 0 ? 0 : gidx-1);
130 assert(gidx >= 0 && gidx < groups.length);
131 groups[gidx].activate();
132 postScreenUpdate();
133 })("group_prev", "go to previous group");
135 conRegFunc!(() {
136 if (groups.length == 0) return;
137 auto gidx = getActiveGroupIndex();
138 if (gidx == groups.length-1) return;
139 if (gidx >= 0) groups[gidx].deactivate();
140 gidx = (gidx < 0 ? 0 : gidx+1);
141 assert(gidx >= 0 && gidx < groups.length);
142 groups[gidx].activate();
143 postScreenUpdate();
144 })("group_next", "go to next group");
146 conRegFunc!(() {
147 foreach (immutable gidx, Group g; groups) {
148 if (g.active) {
149 if (g.markAsUnread) postScreenUpdate();
150 return;
153 })("mark_unread", "mark current message as unread");
155 conRegFunc!(() {
156 foreach (immutable gidx, Group g; groups) {
157 if (g.active) {
158 if (g.markAsRead) postScreenUpdate();
159 return;
162 })("mark_read", "mark current message as read");
164 conRegFunc!((bool allowNextGroup=false) {
165 foreach (immutable _; 0..groups.length) {
166 uint actgidx = uint.max;
167 foreach (immutable gidx, Group g; groups) {
168 if (!g.active) continue;
169 actgidx = cast(uint)gidx;
170 if (g.moveToNextUnread) {
171 postScreenUpdate();
172 return;
175 if (!allowNextGroup) break;
176 // move to next group
177 if (actgidx == uint.max) {
178 actgidx = 0;
179 } else {
180 groups[actgidx].releaseContent();
181 groups[actgidx].active = false;
182 actgidx = (actgidx+1)%groups.length;
184 groups[actgidx].active = true;
186 })("next_unread", "move to next unread message; bool arg: can skip to next group?");
188 conRegFunc!(() {
189 if (articleTextTopLine > 0) {
190 articleTextTopLine -= (VBufHeight-guiThreadListHeight-3*10-4)/gxTextHeightUtf;
191 if (articleTextTopLine < 0) articleTextTopLine = 0;
192 postScreenUpdate();
194 })("artext_page_up", "do pageup on article text");
196 conRegFunc!(() {
197 articleTextTopLine += (VBufHeight-guiThreadListHeight-3*10-4)/gxTextHeightUtf;
198 postScreenUpdate();
199 })("artext_page_down", "do pagedown on article text");
201 conRegFunc!(() {
202 if (auto g = getActiveGroup) {
203 if (g.moveUp) postScreenUpdate();
205 })("article_prev", "go to previous article");
207 conRegFunc!(() {
208 if (auto g = getActiveGroup) {
209 if (g.moveDown) postScreenUpdate();
211 })("article_next", "go to next article");
213 conRegFunc!(() {
214 if (auto g = getActiveGroup) {
215 if (g.movePageUp) postScreenUpdate();
217 })("article_pgup", "artiles list: page up");
219 conRegFunc!(() {
220 if (auto g = getActiveGroup) {
221 if (g.movePageDown) postScreenUpdate();
223 })("article_pgdown", "artiles list: page down");
225 conRegFunc!(() {
226 if (auto g = getActiveGroup) {
227 if (g.scrollUp) postScreenUpdate();
229 })("article_scroll_up", "scroll article list up");
231 conRegFunc!(() {
232 if (auto g = getActiveGroup) {
233 if (g.scrollDown) postScreenUpdate();
235 })("article_scroll_down", "scroll article list up");
237 conRegFunc!(() {
238 if (auto g = getActiveGroup) {
239 if (g.moveToFirst) postScreenUpdate();
241 })("article_to_first", "go to first article");
243 conRegFunc!(() {
244 if (auto g = getActiveGroup) {
245 if (g.moveToLast) postScreenUpdate();
247 })("article_to_last", "go to last article");
249 conRegFunc!(() {
250 if (auto g = getActiveGroup) {
251 g.withBase(delegate (abase) {
252 uint idx = g.curidx;
253 if (idx > 0 && idx < g.length) {
254 auto depth = abase[g.baseidx(idx)].depth;
255 if (depth > 0) {
256 --depth;
257 while (idx < g.length && abase[g.baseidx(idx)].depth != depth) --idx;
258 if (idx < g.length) {
259 g.curidx = idx;
260 postScreenUpdate();
261 return;
267 })("article_to_parent", "jump to parent article, if any");
269 conRegFunc!(() {
270 foreach (Group g; groups) g.forceUpdating();
271 if (!updateInProgress) {
272 updateInProgress = true;
273 //vbwinLocked = true;
274 updateThreadId.send(UpThreadCommand.StartUpdate);
276 })("update_all", "mark all groups for updating");
279 // //////////////////////////////////////////////////////////////////// //
280 // debug
281 conRegFunc!(() {
282 if (auto g = getActiveGroup) {
283 static import iv.vfs.io;
285 g.withBase(delegate (abase) {
286 auto sk = new SocketNNTP("news.digitalmars.com");
287 scope(exit) {
288 if (sk.active) sk.quit();
289 sk.close();
292 sk.selectGroup(abase.groupname);
294 if (sk.emptyGroup) return;
296 uint stnum = abase.maxnum+1;
298 if (abase.length == 0) stnum = (sk.hiwater > 1023 ? sk.hiwater-1023 : 0);
299 if (stnum > sk.hiwater) { conwriteln("no new articles"); return; }
301 conwriteln(sk.hiwater+1-stnum, " new articles");
303 // download new articles
304 foreach (immutable uint anum; stnum..sk.hiwater+1) {
305 import std.conv : to;
306 iv.vfs.io.write("\r[", anum, "/", sk.hiwater, "] ... \x1b[K");
307 auto art = sk.getArticle(anum);
308 if (!art.valid) { iv.vfs.io.writeln("SKIP"); continue; }
309 iv.vfs.io.write("OK");
310 art.flags |= Article.Flag.Unread;
311 abase.insert(art);
312 //abase.selfCheck();
314 iv.vfs.io.writeln;
316 abase.selfCheck();
317 abase.writeUpdates();
319 g.buildVisibleList();
322 postScreenUpdate();
324 })("group_update", "update current group");
326 conRegFunc!(() {
327 if (auto g = getActiveGroup) {
328 if (g.curidxValid) {
329 g.withBase(delegate (abase) {
330 auto art = abase[g.baseidx(g.curidx)];
331 if (art !is null) {
332 conwriteln("============================");
334 conwriteln("replyto: ", art.replyto, "|");
335 if (art.replyto.length) {
336 auto arr = g.mbase[art.replyto];
337 if (arr !is null) conwriteln("*** ", arr.from, " ***");
340 foreach (string s; art.headers) conwriteln(" ", s);
341 conwriteln("---------------");
342 conwriteln(" ", art.fromname, " <", art.frommail, ">");
343 conwrite(" ");
344 Utf8DecoderFast dc;
345 foreach (char ch; art.fromname) {
346 if (dc.decode(cast(ubyte)ch)) {
347 if (dc.codepoint > 127 || dc.codepoint < 32) conwritef!" \\u%04X "(cast(uint)dc.codepoint);
348 else conwrite(cast(char)dc.codepoint);
351 conwriteln();
356 })("article_dump_headers", "dump article headers");
358 conRegFunc!(() {
359 if (auto g = getActiveGroup) {
360 g.withBase(delegate (abase) {
361 uint idx = g.curidx;
362 if (idx >= g.length) {
363 idx = 0;
364 } else if (auto art = abase[g.baseidx(idx)]) {
365 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
366 idx = (idx+1)%g.length;
369 foreach (immutable _; 0..g.length) {
370 auto art = abase[g.baseidx(idx)];
371 if (art !is null) {
372 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
373 g.curidx = cast(int)idx;
374 postScreenUpdate();
375 return;
378 idx = (idx+1)%g.length;
382 })("find_mine", "find mine article");
384 conRegFunc!(() {
385 new FontWindow();
386 })("dbg_font_window", "show window with font");
388 conRegVar!dbg_dump_keynames("dbg_dump_key_names", "dump key names");
392 // ////////////////////////////////////////////////////////////////////////// //
393 __gshared IncomingEmailMessage eml;
394 __gshared string[] emlines;
397 void glUpdateTexture () {
398 import iv.glbinds;
400 zxtexbuf[] = 0;
401 clipReset();
403 gxFillRect(0, 0, guiGroupListWidth, VBufHeight, gxRGB!(20, 20, 20));
404 gxVLine(guiGroupListWidth, 0, VBufHeight, gxRGB!(255, 255, 255));
406 gxFillRect(guiGroupListWidth+1, 0, VBufWidth, guiThreadListHeight, gxRGB!(15, 15, 15));
407 gxHLine(guiGroupListWidth+1, guiThreadListHeight, VBufWidth, gxRGB!(255, 255, 255));
409 void drawArticle (Group g, ref Article art, string title) {
410 import core.stdc.stdio : snprintf;
411 import std.format : format;
412 import std.datetime;
413 char[128] tbuf;
414 const(char)[] tbufs;
416 void xfmt (string s, const(char)[][] strs...) {
417 int dpos = 0;
418 void puts (const(char)[] s...) {
419 foreach (char ch; s) {
420 if (dpos >= tbuf.length) break;
421 tbuf[dpos++] = ch;
424 while (s.length) {
425 if (strs.length && s.length > 1 && s[0] == '%' && s[1] == 's') {
426 puts(strs[0]);
427 strs = strs[1..$];
428 s = s[2..$];
429 } else {
430 puts(s[0]);
431 s = s[1..$];
434 tbufs = tbuf[0..dpos];
437 if (!art.valid || !art.contentLoaded) {
438 lastArticleText.clear();
439 articleTextTopLine = 0;
440 eml = null;
441 emlines = null;
444 if (!lastArticleText.equal(g, art)) {
445 lastArticleText.set(g, art);
446 articleTextTopLine = 0;
447 eml = null;
448 emlines = null;
451 clipX0 = guiGroupListWidth+2;
452 clipX1 = VBufWidth-1;
453 clipY0 = guiThreadListHeight+1;
454 clipY1 = VBufHeight-1;
456 gxFillRect(clipX0, clipY0, clipX1-clipX0+1, 3*gxTextHeightUtf+2, gxRGB!(30, 30, 30));
457 //gxDrawTextUtf(clipX0+1, clipY0+0*gxTextHeightUtf+1, "From: %s <%s>".format(art.fromname, art.frommail), gxRGB!(0, 128, 128));
458 //gxDrawTextUtf(clipX0+1, clipY0+1*gxTextHeightUtf+1, "Subject: %s".format(art.subj), gxRGB!(0, 128, 128));
459 xfmt("From: %s <%s>", art.fromname, art.frommail);
460 gxDrawTextUtf(clipX0+1, clipY0+0*gxTextHeightUtf+1, tbufs, gxRGB!(0, 128, 128));
461 xfmt("Subject: %s", art.subj);
462 gxDrawTextUtf(clipX0+1, clipY0+1*gxTextHeightUtf+1, tbufs, gxRGB!(0, 128, 128));
464 auto t = SysTime.fromUnixTime(art.time);
465 //string s = "Date: %04d/%02d/%02d %02d:%02d:%02d".format(t.year, t.month, t.day, t.hour, t.minute, t.second);
466 auto tlen = snprintf(tbuf.ptr, tbuf.length, "Date: %04d/%02d/%02d %02d:%02d:%02d", t.year, t.month, t.day, t.hour, t.minute, t.second);
467 gxDrawTextUtf(clipX0+1, clipY0+2*gxTextHeightUtf+1, tbuf[0..tlen], gxRGB!(0, 128, 128));
470 //TODO: text reflow
471 int y = clipY0+3*gxTextHeightUtf+2;
472 immutable sty = y;
474 if (eml is null) {
475 eml = new IncomingEmailMessage(art.lines);
477 conwriteln("=================");
478 conwriteln("from: ", eml.from);
479 conwriteln("subj: ", eml.subject);
480 conwriteln(eml.textMessageBody);
481 conwriteln("=================");
483 emlines = null;
484 string s = eml.textMessageBody;
485 while (s.length > 0) {
486 auto eolpos = s.indexOf('\n');
487 if (eolpos < 0) { emlines ~= s.xstrip; break; }
488 emlines ~= s[0..eolpos].xstrip;
489 s = s[eolpos+1..$];
491 while (emlines.length && emlines[$-1].length == 0) emlines.length -= 1;
494 int lines = (clipY1-y)/gxTextHeightUtf;
495 if (lines < 1 || emlines.length <= lines) {
496 articleTextTopLine = 0;
497 } else {
498 if (articleTextTopLine+lines > emlines.length) {
499 articleTextTopLine = cast(int)emlines.length-lines;
500 if (articleTextTopLine < 0) articleTextTopLine = 0;
504 uint idx = articleTextTopLine;
505 while (idx < emlines.length && y < VBufHeight) {
506 int qlevel = 0;
507 string s = emlines[idx];
508 uint clr = gxRGB!(0, 128, 0);
510 foreach (char ch; s) {
511 if (ch <= ' ') continue;
512 if (ch != '>') break;
513 ++qlevel;
516 clr = gxRGB!( 0, 128+40, 0);
517 if (qlevel) {
518 final switch (qlevel%2) {
519 case 0: clr = gxRGB!(128, 128, 0); break;
520 case 1: clr = gxRGB!( 0, 128, 128); break;
524 gxDrawTextUtf(clipX0+1, y, s, clr);
526 if (clipY1-y < gxTextHeightUtf && emlines.length-idx > 0) {
527 // draw "down" indicator
528 gxDrawTextOutP(clipX1-gxTextWidthP(triangleUpStr)-3, clipY1-7, triangleUpStr, gxRGB!(255, 255, 255), gxRGB!(0, 69, 69));
531 ++idx;
532 y += gxTextHeightUtf;
535 if (articleTextTopLine > 0) {
536 gxDrawTextOutP(clipX1-gxTextWidthP(triangleDownStr)-3, sty, triangleDownStr, gxRGB!(255, 255, 255), gxRGB!(0, 69, 69));
539 if (title.length) {
540 foreach (immutable dy; clipY0+3*gxTextHeightUtf+2..clipY1+1) {
541 foreach (immutable dx; clipX0..clipX1+1) {
542 if ((dx^dy)&1) gxPutPixel(dx, dy, gxRGB!(0, 0, 80));
546 int tx = clipX0+(clipWidth-gxTextWidthScaledUtf(3, title))/2-1;
547 int ty = clipY0+(clipHeight-3*gxTextHeightUtf)/2-1;
548 foreach (immutable dy; -1..1) {
549 foreach (immutable dx; -1..1) {
550 gxDrawTextScaledUtf(3, tx+dx, ty+dy, title, 0);
553 gxDrawTextScaledUtf(3, tx, ty, title, gxRGB!(255, 0, 0));
557 void drawThreadList (Group g) {
558 g.withBase(delegate (abase) {
559 g.makeCurrentVisible();
561 clipX0 = guiGroupListWidth+2;
562 clipX1 = VBufWidth-1-4;
563 clipY0 = 0;
564 clipY1 = guiThreadListHeight-1;
565 immutable uint origX0 = clipX0;
566 immutable uint origX1 = clipX1;
567 immutable uint origY0 = clipY0;
568 immutable uint origY1 = clipY1;
569 int y = -g.ytopofs;
570 //conwriteln(g.msgtop, " : ", g.list.length);
571 uint idx = g.msgtop;
572 while (idx < g.length && y < guiThreadListHeight) {
573 import std.format : format;
574 import std.datetime;
576 if (y >= guiThreadListHeight) break;
577 if (idx >= g.length) break;
579 clipX0 = origX0;
580 clipX1 = origX1;
582 //conwriteln(idx, " : ", g.list.length);
583 if (idx == g.curidx) gxFillRect(clipX0, y, clipWidth-1, gxTextHeightUtf, gxRGB!(0, 127, 127));
584 ++clipX0;
585 --clipX1;
587 auto art = abase[g.baseidx(idx)];
589 //uint clr = (idx != g.curidx ? gxRGB!(255, 127, 0) : gxRGB!(255, 255, 255));
590 uint clr = (art.unread ? gxRGB!(255, 255, 255) : gxRGB!(255-60, 127-60, 0));
591 uint clr1 = (art.unread ? gxRGB!(255, 255, 0) : gxRGB!(255-60-40, 127-60-40, 0));
593 if (!art.unread) {
594 if (g.twited(idx).length) { clr = gxRGB!(60, 0, 0); clr1 = gxRGB!(60, 0, 0); }
595 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) { clr = gxRGB!(0, 190, 0); clr1 = gxRGB!(0, 190-40, 0); }
598 auto t = SysTime.fromUnixTime(art.time);
599 //string s = "%04d/%02d/%02d %02d:%02d".format(t.year, t.month, t.day, t.hour, t.minute);
601 import core.stdc.stdio : snprintf;
602 char[128] tmpbuf;
603 //string s = "%02d/%02d %02d:%02d".format(t.month, t.day, t.hour, t.minute);
604 auto len = snprintf(tmpbuf.ptr, tmpbuf.length, "%02d/%02d/%02d %02d:%02d", t.year%100, t.month, t.day, t.hour, t.minute);
605 gxDrawTextUtf(clipX1-gxTextWidthUtf(tmpbuf[0..len]), y, tmpbuf[0..len], clr);
608 string from = art.fromname;
609 if (from.length > 2 && from[0] == '"' && from[$-1] == '"') {
610 from = from[1..$-1].xstrip;
611 if (from.length == 0) from = "anonymous";
614 auto vp = from.indexOf(" via Digitalmars-");
615 if (vp > 0) {
616 from = from[0..vp].xstrip;
617 if (from.length == 0) from = "anonymous";
621 clipX1 -= 13*6+4;
622 gxDrawTextUtf(clipX1-22*6, y, from, clr);
623 gxDrawTextUtf(clipX1-22*6+gxTextWidthUtf(from)+4, y, "<", clr1);
624 gxDrawTextUtf(clipX1-22*6+gxTextWidthUtf(from)+4+gxTextWidthUtf("<")+1, y, art.frommail, clr1);
625 gxDrawTextUtf(clipX1-22*6+gxTextWidthUtf(from)+4+gxTextWidthUtf("<")+1+gxTextWidthUtf(art.frommail)+1, y, ">", clr1);
627 clipX1 -= 22*6+4;
628 gxDrawTextUtf(clipX0+art.depth*3, y, art.subj, clr);
629 foreach (immutable dx; 0..art.depth) gxPutPixel(clipX0+1+dx*3, y+gxTextHeightUtf/2, gxRGB!(70, 70, 70));
631 ++idx;
632 y += gxTextHeightUtf;
635 // draw progressbar
637 //if (idx > g.list.length) idx = cast(uint)g.list.length;
638 clipX0 = origX0;
639 clipX1 = origX1+4;
640 clipY0 = origY0;
641 clipY1 = origY1;
642 int hgt = clipY1-clipY0+1-4;
643 int pix = cast(int)(cast(long)hgt*idx/g.length);
644 if (pix > hgt) pix = hgt;
645 gxVLine(clipX1-2, clipY0+2, pix, gxRGB!(160, 160, 160));
646 // frame
647 gxVLine(clipX1-3, clipY0+2, hgt, gxRGB!(220, 220, 220));
648 gxVLine(clipX1-1, clipY0+2, hgt, gxRGB!(220, 220, 220));
649 gxHLine(clipX1-2, clipY0+1, 1, gxRGB!(220, 220, 220));
650 gxHLine(clipX1-2, clipY1-1, 1, gxRGB!(220, 220, 220));
653 if (g.curidx < g.length) {
654 abase.loadContent(g.baseidx(g.curidx));
655 drawArticle(g, *abase[g.baseidx(g.curidx)], g.twited(g.curidx));
660 foreach (immutable idx, Group g; groups) {
661 int ofsx = 2;
662 int ofsy = 1+cast(int)idx*(gxTextHeightUtf+2);
663 clipReset();
664 if (g.active) gxFillRect(0, ofsy-1, guiGroupListWidth, (gxTextHeightUtf+2), gxRGB!(0, 127, 127));
665 clipX0 = ofsx-1;
666 clipY0 = ofsy;
667 clipX1 = guiGroupListWidth-3;
668 uint clr = (g.unreadCount ? gxRGB!(255, 255, 0) : gxRGB!(255, 127, 0));
669 if (g.uiFlagUpdating) clr = gxRGB!(0, 255, 255);
670 gxDrawTextOutP(ofsx, ofsy, g.groupname, clr, gxRGB!(0, 0, 0));
671 if (g.active) {
672 drawThreadList(g);
676 foreach (immutable idx, SubWindow w; subwins) {
677 if (idx == subwins.length-1 && w.immuneToLock) break;
678 w.onPaint();
681 if (vbwinLocked) {
682 clipReset();
683 foreach (immutable y; 0..VBufHeight) {
684 foreach (immutable x; 0..VBufWidth) {
685 if ((x^y)&1) gxPutPixel(x, y, gxRGB!(0, 0, 99));
690 if (subwins.length && subwins[$-1].immuneToLock) subwins[$-1].onPaint();
692 if (zxtexid) {
693 glBindTexture(GL_TEXTURE_2D, zxtexid);
694 glTexSubImage2D(GL_TEXTURE_2D, 0, 0/*x*/, 0/*y*/, VBufWidth, VBufHeight, GLTexType, GL_UNSIGNED_BYTE, zxtexbuf.ptr);
695 //glBindTexture(GL_TEXTURE_2D, 0);
700 // ////////////////////////////////////////////////////////////////////////// //
701 class TitlePrompt : SubWindow {
702 string fullname;
703 string name;
704 string msgid;
705 LineEdit etitle;
707 @property string title () const pure nothrow @safe @nogc { return etitle.str; }
709 this (string afullname, string aname, string amsgid, string atitle=null) {
710 super(260, 28);
711 if (hasWindowClass(this)) return;
712 fullname = afullname;
713 name = aname;
714 msgid = amsgid;
715 etitle = new LineEdit();
716 etitle.str = atitle;
717 etitle.active = true;
718 add();
721 override void close () { super.close(); }
723 override void onPaint () {
724 setupClip();
725 gxDrawWindow(name, gxRGB!(255, 255, 255), gxRGB!(0, 0, 0), gxRGB!(0, 0, 180));
727 setupClientClip();
728 clipX0 += 4;
729 clipX1 -= 4;
730 clipY0 += 2;
731 etitle.onPaint();
734 override void onKey (KeyEvent event) {
735 if (!event.pressed) return;
736 if (event == "Escape") { close(); return; }
737 if (event == "Enter") {
738 twits.update(name, msgid, title, fullname);
739 if (!vbwinLocked) {
740 if (auto g = getActiveGroup) {
741 g.withBase(delegate (abase) {
742 if (g.curidx < g.length && abase[g.baseidx(g.curidx)].depth == 0) {
743 g.moveToNextThread();
745 g.buildVisibleList();
749 close();
750 return;
752 etitle.onKey(event);
755 override void onMouse (MouseEvent event) {
758 override void onChar (dchar ch) {
759 etitle.onChar(ch);
764 // ////////////////////////////////////////////////////////////////////////// //
765 class PostEditor : SubWindow {
766 string groupname;
767 string replyid; // replyto
768 string[] hdrs;
769 Editor editor;
770 int topline;
771 LineEdit esubj;
772 bool subjActive;
774 @property string subj () const pure nothrow @safe @nogc { return esubj.str; }
776 bool doSend () {
777 if (subj.length == 0) return false;
779 try {
780 import std.digest.sha;
781 import std.datetime;
782 import std.uuid;
784 conwriteln("sending to '", groupname, "'");
786 // build references
787 string[] refs;
788 if (replyid.length) {
789 void addRef (string s) {
790 s = s.xstrip;
791 if (s.length == 0) return;
792 bool firstRef = (refs.length == 0);
793 if (refs.length == 0) refs ~= "References:";
794 if (!firstRef && refs[$-1].length+s.length > 68) {
795 refs ~= "\t"~s;
796 } else {
797 refs[$-1] ~= " "~s;
800 void procRefs (string s) {
801 for (;;) {
802 s = s.xstrip;
803 if (s.length == 0) break;
804 usize pos = 0;
805 while (pos < s.length && s[pos] > ' ') ++pos;
806 assert(pos > 0);
807 addRef(s[0..pos]);
808 s = s[pos..$];
811 string preplyto;
812 bool lastWasRef = false;
813 foreach (string hss; hdrs) {
814 if (hss.startsWithCI("References:")) {
815 procRefs(hss[11..$]);
816 lastWasRef = true;
817 } else if (hss[0] <= ' ') {
818 if (lastWasRef) procRefs(hss);
819 } else {
820 lastWasRef = false;
821 if (preplyto.length == 0 && hss.startsWithCI("In-Reply-To:")) {
822 preplyto = hss[12..$].xstrip;
826 if (refs.length == 0 && preplyto.length) procRefs(preplyto);
827 addRef(replyid);
828 conwriteln("============== REFS ==============");
829 foreach (string s; refs) conwriteln(" |", s, "|");
830 conwriteln("-----------------------");
834 SHA256 hash;
835 hash.put(cast(const(ubyte)[])Clock.currTime().toString);
836 hash.put(cast(const(ubyte)[])groupname);
837 hash.put(cast(const(ubyte)[])replyid);
838 hash.put(cast(const(ubyte)[])subj);
839 foreach (immutable idx; 0..editor.lineCount) hash.put(cast(const(ubyte)[])editor[idx]);
840 auto hashres = hash.finish();
841 string hashstr = toHexString!(LetterCase.lower)(hashres);
843 UUID id = randomUUID();
844 string hashstr = toHexString!(LetterCase.lower)(id.data);
845 hashstr = "<"~hashstr~"@dingo>";
846 conwriteln(" message id: ", hashstr);
847 conwriteln(" reply to : ", replyid);
849 SocketNNTP sk;
850 try {
851 sk = new SocketNNTP("news.digitalmars.com");
852 } catch (Exception e) {
853 conwriteln("connection error: ", e.msg);
854 return false;
856 scope(exit) {
857 if (sk.active) sk.quit();
858 sk.close();
861 sk.selectGroup(groupname);
863 sk.doSend("%s", "POST");
864 sk.doSend("%s", "From: ketmar <ketmar@ketmar.no-ip.org>");
865 sk.doSend("Newsgroups: %s", groupname);
866 sk.doSend("Subject: %s", subj);
867 sk.doSend("Message-ID: %s", hashstr);
868 sk.doSend("%s", "Mime-Version: 1.0");
869 sk.doSend("%s", "Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no");
870 sk.doSend("%s", "Content-Transfer-Encoding: 8bit");
871 sk.doSend("%s", "User-Agent: dingo");
872 foreach (string s; refs) sk.doSend("%s", s);
873 if (replyid.length) sk.doSend("In-Reply-To: %s", replyid);
874 sk.doSend("%s", "");
875 foreach (immutable idx; 0..editor.lineCount) {
876 string s = editor[idx];
877 if (s.length > 0 && s[0] == '.') s = "."~s;
878 sk.doSend("%s", s);
880 sk.doSend("%s", ".");
882 auto ln = sk.readLine;
883 conwriteln(ln); // 340 Ok, recommended message-ID <o7dq4o$mpm$1@digitalmars.com>
885 if (ln.length == 0 || ln[0] != '3') throw new Exception(ln.idup);
887 foreach (Group g; groups) {
888 if (g.groupname == groupname) g.forceUpdating();
891 if (!updateInProgress) {
892 updateInProgress = true;
893 //vbwinLocked = true;
894 updateThreadId.send(UpThreadCommand.StartUpdate);
897 return true;
898 } catch (Exception e) {
899 conwriteln("SENDING ERROR: ", e.msg);
902 return false;
905 this() (string agroupname, in auto ref Article art) {
906 super(506, 253);
907 if (hasWindowClass(this)) return;
908 if (agroupname.length == 0) return;
909 editor = new Editor();
910 esubj = new LineEdit();
911 if (art.valid) {
912 esubj.str = "Re: "~art.subj;
913 replyid = art.msgid;
914 hdrs = art.headers.dup;
916 if (art.valid && art.contentLoaded) {
917 editor.addLine(art.fromname~" wrote:");
918 //editor.addLine("");
919 foreach (string s; art.text) {
920 auto qi = calcQuote(s);
921 ++qi.level;
922 if (qi.level > 32) qi.level = 32;
923 s = ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"[0..qi.level]~" "~s[qi.length..$];
924 editor.addLine(s);
926 editor.addLine("");
927 editor.reformat();
929 if (editor.lineCount == 0) editor.addLine("");
930 groupname = agroupname;
931 add();
934 final void textClip () {
935 setupClientClip();
936 clipX0 += 1;
937 clipX1 -= 1+4;
938 clipY0 += 12+6;
939 clipY1 -= 1;
942 final void drawSubj () {
943 setupClientClip(); // the easiest way again
944 //gxFillRect(clipX0, clipY0, clipWidth, 14, gxRGB!(0, 0, 120));
945 clipY0 += 1;
946 gxFillRect(clipX0+2, clipY0, clipWidth-4, 10, (subjActive ? gxRGB!(0, 0, 0) : gxRGB!(20, 20, 20)));
947 clipX0 += 4;
948 clipX1 -= 4;
949 clipY0 += 1;
950 esubj.active = subjActive;
951 esubj.onPaint();
954 final void makeCurVisible () {
955 textClip(); // the easiest way to get the size
956 int lvis = clipHeight/gxTextHeightUtf;
957 if (lvis < 1) lvis = 1; // just in case
958 int ltop = topline;
959 int lbot = topline+lvis-1;
960 int cy = editor.cury;
961 if (cy < ltop) {
962 topline = cy;
963 } else if (cy > lbot) {
964 topline = cy-lvis+1;
965 if (topline < 0) topline = 0;
969 final void drawScrollBar () {
970 textClip(); // the easiest way again
971 clipX1 += 4;
972 // frame
973 gxVLine(clipX1-2, clipY0+1, clipHeight-2, gxRGB!(220, 220, 220));
974 gxVLine(clipX1-0, clipY0+1, clipHeight-2, gxRGB!(220, 220, 220));
975 gxHLine(clipX1-1, clipY0, 1, gxRGB!(220, 220, 220));
976 gxHLine(clipX1-1, clipY1, 1, gxRGB!(220, 220, 220));
977 int pix = (clipHeight-2)*editor.cury/editor.lineCount;
978 if (pix > clipHeight-2) pix = clipHeight-2;
979 gxVLine(clipX1-1, clipY0+1, pix, gxRGB!(160, 160, 160));
982 override void onPaint () {
983 setupClip();
985 gxDrawWindow(groupname, gxRGB!(255, 255, 255), gxRGB!(0, 0, 0), gxRGB!(0, 0, 180));
987 textClip();
989 makeCurVisible();
991 int lidx = topline;
992 int y = clipY0;
993 while (lidx < editor.lineCount && y <= clipY1) {
994 string s = editor[lidx];
995 int qlevel = editor.quoteLevel(lidx);
996 uint clr = gxRGB!(220, 220, 0);
997 //uint markBgClr = gxRGB!(227, 0, 195);
998 uint markFgClr = gxRGB!(255, 255, 255);
999 uint markBgClr = gxRGB!(0, 160, 160);
1000 if (qlevel) {
1001 final switch (qlevel%2) {
1002 case 0: clr = gxRGB!(128, 128, 0); break;
1003 case 1: clr = gxRGB!( 0, 128, 128); break;
1006 if (!editor.lineHasMark(lidx)) {
1007 gxDrawTextUtf(clipX0, y, s, clr);
1008 } else {
1009 int xx = clipX0;
1010 int cx = 0;
1011 utfByLocal(editor[lidx], delegate (char ch) {
1012 int w = gxCharWidthP(ch)+1;
1013 if (editor.isMarked(cx, lidx)) {
1014 gxFillRect(xx, y, w-(editor.isMarked(cx+1, lidx) ? 0 : 1), gxTextHeightUtf, markBgClr);
1015 gxDrawCharP(xx, y, ch, markFgClr);
1016 } else {
1017 gxDrawCharP(xx, y, ch, clr);
1019 xx += w;
1020 ++cx;
1023 if (!subjActive && lidx == editor.cury) {
1024 int xpos = gxTextWidthUtf(s.utfleft(editor.curx));
1025 drawTextCursor(clipX0+xpos, y);
1027 ++lidx;
1028 y += gxTextHeightUtf;
1030 if (!subjActive && editor.cury >= editor.lineCount) drawTextCursor(clipX0, y);
1031 drawScrollBar();
1032 drawSubj();
1033 if (!subjActive) postCurBlink();
1036 override void onKey (KeyEvent event) {
1037 if (!event.pressed) return;
1038 if (event == "C-Escape" || event == "M-Escape" || event == "C-C" || event == "C-Q") { close(); return; }
1039 if (event == "C-Enter") {
1040 if (!vbwinLocked) {
1041 if (doSend()) close();
1043 return;
1045 if (event == "Tab") { subjActive = !subjActive; return; }
1046 if (!subjActive) {
1047 if (event == "Escape") { editor.putChar(editor.SpecCh.ResetMark); return; }
1048 if (event == "C-Space") { editor.putChar(editor.SpecCh.PutMark); return; }
1049 if (event == "Enter" || event == "PadEnter") { editor.putChar(editor.SpecCh.Enter); return; }
1050 if (event == "Left" || event == "Pad4") { editor.putChar(editor.SpecCh.Left); return; }
1051 if (event == "Right" || event == "Pad6") { editor.putChar(editor.SpecCh.Right); return; }
1052 if (event == "Up" || event == "Pad8") { editor.putChar(editor.SpecCh.Up); return; }
1053 if (event == "Down" || event == "Pad2") { editor.putChar(editor.SpecCh.Down); return; }
1054 if (event == "Delete" || event == "PadDot") { editor.putChar(editor.SpecCh.Delete); return; }
1055 if (event == "Home" || event == "Pad7") { editor.putChar(editor.SpecCh.Home); return; }
1056 if (event == "End" || event == "Pad1") { editor.putChar(editor.SpecCh.End); return; }
1057 if (event == "C-Y") { editor.putChar(editor.SpecCh.KillLine); return; }
1058 if (event == "C-PageUp") { editor.gotoTop(); return; }
1059 if (event == "C-PageDown") { editor.gotoBottom(); return; }
1060 if (event == "PageUp") {
1061 textClip();
1062 foreach (immutable _; 0..clipHeight/gxTextHeightUtf) editor.putChar(editor.SpecCh.Up);
1063 return;
1065 if (event == "PageDown") {
1066 textClip();
1067 foreach (immutable _; 0..clipHeight/gxTextHeightUtf) editor.putChar(editor.SpecCh.Down);
1068 return;
1070 if (event == "S-Insert") {
1071 getClipboardText(vbwin, delegate (in char[] text) {
1072 if (!closed) editor.putUtf(text[]);
1074 return;
1076 if (event == "C-Insert") {
1077 string ct = editor.getSelectionText();
1078 if (ct.length > 0) {
1079 setClipboardText(vbwin, ct);
1080 setPrimarySelection(vbwin, ct);
1082 editor.putChar(editor.SpecCh.ResetMark);
1083 return;
1085 } else {
1086 esubj.onKey(event);
1090 override void onMouse (MouseEvent event) {
1093 override void onChar (dchar ch) {
1094 if (!subjActive) {
1095 if (ch == 8) { editor.putChar(ch); return; }
1096 if (ch < ' ' || ch == 127) return;
1097 editor.putChar(ch);
1098 } else {
1099 esubj.onChar(ch);
1105 // ////////////////////////////////////////////////////////////////////////// //
1106 class FontWindow : SubWindow {
1107 int cx, cy;
1109 this () {
1110 super(16*14+4, 16*14+4+10);
1111 if (hasWindowClass(this)) return;
1112 add();
1115 override void onPaint () {
1116 import core.stdc.stdio : snprintf;
1117 char[64] buf;
1119 auto tlen = snprintf(buf.ptr, buf.length, "Font: \\x%02x (%d)", cast(int)(cy*16+cx), cast(int)(cy*16+cx));
1121 setupClip();
1122 gxDrawWindow(buf[0..tlen], gxRGB!(255, 255, 255), gxRGB!(0, 0, 0), gxRGB!(0, 0, 180));
1124 setupClientClip();
1125 clipX0 += 2;
1126 clipY0 += 0;
1127 foreach (immutable int dy; 0..16) {
1128 foreach (immutable int dx; 0..16) {
1129 if (cx == dx && cy == dy) gxFillRect(clipX0+dx*14, clipY0+dy*14, 12, 13, 0);
1130 gxDrawCharM(clipX0+dx*14+2, clipY0+dy*14+2, cast(char)(dy*16+dx),
1131 ((dx^dy)&1 ? gxRGB!(255, 255, 255) : gxRGB!(255, 255, 0)));
1136 override void onKey (KeyEvent event) {
1137 if (!event.pressed) return;
1138 if (event == "Escape" || event == "C-Q") { close(); return; }
1139 if (event == "Left" || event == "Pad4") { cx = (cx+15)%16; return; }
1140 if (event == "Right" || event == "Pad6") { cx = (cx+1)%16; return; }
1141 if (event == "Up" || event == "Pad8") { cy = (cy+15)%16; return; }
1142 if (event == "Down" || event == "Pad2") { cy = (cy+1)%16; return; }
1143 if (event == "Home" || event == "Pad7") { cx = 0; return; }
1144 if (event == "End" || event == "Pad1") { cx = 15; return; }
1145 if (event == "PageUp" || event == "Pad9") { cy = 0; return; }
1146 if (event == "PageDown" || event == "Pad3") { cy = 15; return; }
1149 override void onMouse (MouseEvent event) {}
1150 override void onChar (dchar ch) {}
1154 // ////////////////////////////////////////////////////////////////////////// //
1155 __gshared LockFile mainLockFile;
1158 void main (string[] args) {
1159 mainLockFile = LockFile(".dingo.lock");
1160 if (!mainLockFile.tryLock) { mainLockFile.close(); assert(0, "already running"); }
1162 sdpyWindowClass = "NNTPReaderDingo";
1163 glconShowKey = "C-Grave";
1165 initConsole();
1167 twits = new TwitList();
1168 twits.load();
1170 threadtwits = new ThreadTwitList();
1171 threadtwits.load();
1173 //concmdf!"exec \"%q/zxemut.rc\" tan"(configDir);
1174 concmd("exec nntp.rc tan");
1175 conProcessQueue(); // load config
1176 conProcessArgs!true(args);
1178 vbufEffScale = VBufScale;
1179 vbufEffVSync = vbufVSync;
1181 lastWinWidth = winWidthScaled;
1182 lastWinHeight = winHeightScaled;
1184 vbwin = new SimpleWindow(lastWinWidth, lastWinHeight, "Dingo", OpenGlOptions.yes, Resizablity.allowResizing);
1185 vbwin.hideCursor();
1187 vbwin.onFocusChange = delegate (bool focused) {
1188 if (!focused) {
1189 oldMouseButtons = 0;
1191 vbfocused = focused;
1194 vbwin.windowResized = delegate (int wdt, int hgt) {
1195 // TODO: fix gui sizes
1197 if (lastWinWidth == wdt && lastWinHeight == hgt) return;
1198 glconResize(wdt, hgt);
1200 double glwFrac = cast(double)guiGroupListWidth/VBufWidth;
1201 double tlhFrac = cast(double)guiThreadListHeight/VBufHeight;
1203 vbufEffScale = VBufScale;
1204 if (wdt < VBufScale*32) wdt = VBufScale;
1205 if (hgt < VBufScale*32) hgt = VBufScale;
1206 VBufWidth = (wdt+VBufScale-1)/VBufScale;
1207 VBufHeight = (hgt+VBufScale-1)/VBufScale;
1208 zxtexbuf.length = VBufWidth*VBufHeight+4;
1210 guiGroupListWidth = cast(int)(glwFrac*VBufWidth+0.5);
1211 guiThreadListHeight = cast(int)(tlhFrac*VBufHeight+0.5);
1213 if (guiGroupListWidth < 12) guiGroupListWidth = 12;
1214 if (guiThreadListHeight < 16) guiThreadListHeight = 16;
1216 lastWinWidth = wdt;
1217 lastWinHeight = hgt;
1219 // reinitialize OpenGL texture
1221 import iv.glbinds;
1223 enum wrapOpt = GL_REPEAT;
1224 enum filterOpt = GL_NEAREST; //GL_LINEAR;
1225 enum ttype = GL_UNSIGNED_BYTE;
1227 if (zxtexid) glDeleteTextures(1, &zxtexid);
1228 zxtexid = 0;
1229 glGenTextures(1, &zxtexid);
1230 if (zxtexid == 0) assert(0, "can't create OpenGL texture");
1232 //GLint gltextbinding;
1233 //glGetIntegerv(GL_TEXTURE_BINDING_2D, &gltextbinding);
1234 //scope(exit) glBindTexture(GL_TEXTURE_2D, gltextbinding);
1236 glBindTexture(GL_TEXTURE_2D, zxtexid);
1237 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapOpt);
1238 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapOpt);
1239 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filterOpt);
1240 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filterOpt);
1241 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
1242 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
1244 GLfloat[4] bclr = 0.0;
1245 glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, bclr.ptr);
1247 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, VBufWidth, VBufHeight, 0, GLTexType, GL_UNSIGNED_BYTE, zxtexbuf.ptr);
1250 mouseMoved();
1252 glUpdateTexture();
1253 vbwin.redrawOpenGlSceneNow();
1256 vbwin.addEventListener((DoConsoleCommandsEvent evt) {
1257 bool sendAnother = false;
1258 if (!vbwinLocked) {
1259 consoleLock();
1260 scope(exit) consoleUnlock();
1261 //auto ccwasempty = conQueueEmpty();
1262 conProcessQueue();
1263 sendAnother = !conQueueEmpty();
1264 } else {
1265 consoleLock();
1266 scope(exit) consoleUnlock();
1267 sendAnother = !conQueueEmpty();
1269 if (sendAnother) postDoConCommands();
1272 vbwin.addEventListener((HideMouseEvent evt) {
1273 if (isQuitRequested) { vbwin.close(); return; }
1274 if (vbwin.closed) return;
1275 if (!repostHideMouse) vbwin.redrawOpenGlSceneNow(); // this will hide the mouse
1278 vbwin.addEventListener((ScreenUpdateEvent evt) {
1279 //conwriteln("screen ready! ", *cast(void**)&evt);
1280 if (isQuitRequested) { vbwin.close(); return; }
1281 if (vbwin.closed) return;
1282 glUpdateTexture();
1283 vbwin.redrawOpenGlSceneNow();
1286 vbwin.addEventListener((ScreenRepaintEvent evt) {
1287 //conwriteln("screen repaint! ", *cast(void**)&evt);
1288 if (isQuitRequested) { vbwin.close(); return; }
1289 if (vbwin.closed) return;
1290 vbwin.redrawOpenGlSceneNow();
1293 vbwin.addEventListener((QuitEvent evt) {
1294 //conwriteln("quit! ", *cast(void**)&evt);
1295 if (isQuitRequested) { vbwin.close(); return; }
1296 if (vbwin.closed) return;
1297 vbwin.close();
1300 vbwin.addEventListener((UpdatingGroupEvent evt) {
1301 //conwriteln("updating group '", groups[evt.gidx].mbase.groupname);
1302 groups[evt.gidx].uiFlagUpdating = true;
1303 postScreenUpdate();
1306 vbwin.addEventListener((UpdatingGroupCompleteEvent evt) {
1307 //conwriteln("updating group '", groups[evt.gidx].mbase.groupname);
1308 groups[evt.gidx].uiFlagUpdating = false;
1309 groups[evt.gidx].buildVisibleList();
1310 postScreenUpdate();
1311 updateInProgress = false;
1314 vbwin.addEventListener((UpdatingCompleteEvent evt) {
1315 //conwriteln("UPDATE COMPLETE!");
1316 if (vbwinLocked) vbwinLocked = false;
1317 glUpdateTexture();
1318 vbwin.redrawOpenGlSceneNow();
1321 vbwin.addEventListener((CursorBlinkEvent evt) {
1322 //conwriteln("cblink! ", vbwin.eventQueued!CursorBlinkEvent);
1323 glUpdateTexture();
1324 vbwin.redrawOpenGlSceneNow();
1327 vbwin.redrawOpenGlScene = delegate () {
1328 bool resizeWin = false;
1329 bool rebuildTexture = false;
1332 consoleLock();
1333 scope(exit) consoleUnlock();
1335 if (!conQueueEmpty()) postDoConCommands();
1337 if (VBufScale != vbufEffScale) {
1338 // window scale changed
1339 vbufEffScale = VBufScale;
1340 resizeWin = true;
1342 if (vbufEffVSync != vbufVSync) {
1343 vbufEffVSync = vbufVSync;
1344 vbwin.vsync = vbufEffVSync;
1348 if (resizeWin) {
1349 vbwin.resize(winWidthScaled, winHeightScaled);
1350 glconResize(winWidthScaled, winHeightScaled);
1351 rebuildTexture = true;
1354 if (rebuildTexture) glUpdateTexture();
1356 glMatrixMode(GL_PROJECTION); // for ortho camera
1357 glLoadIdentity();
1358 // left, right, bottom, top, near, far
1359 //glViewport(0, 0, w*vbufEffScale, h*vbufEffScale);
1360 //glOrtho(0, w, h, 0, -1, 1); // top-to-bottom
1361 glViewport(0, 0, VBufWidth*vbufEffScale, VBufHeight*vbufEffScale);
1362 glOrtho(0, VBufWidth, VBufHeight, 0, -1, 1); // top-to-bottom
1363 glMatrixMode(GL_MODELVIEW);
1364 glLoadIdentity();
1366 glEnable(GL_TEXTURE_2D);
1367 glDisable(GL_LIGHTING);
1368 glDisable(GL_DITHER);
1369 //glDisable(GL_BLEND);
1370 glDisable(GL_DEPTH_TEST);
1371 //glEnable(GL_BLEND);
1372 //glBlendFunc(GL_SRC_ALPHA, GL_ONE);
1373 //glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
1374 glDisable(GL_BLEND);
1375 //glDisable(GL_STENCIL_TEST);
1377 if (zxtexid) {
1378 immutable w = VBufWidth;
1379 immutable h = VBufHeight;
1381 glColor4f(1, 1, 1, 1);
1382 glBindTexture(GL_TEXTURE_2D, zxtexid);
1383 //scope(exit) glBindTexture(GL_TEXTURE_2D, 0);
1384 glBegin(GL_QUADS);
1385 glTexCoord2f(0.0f, 0.0f); glVertex2i(0, 0); // top-left
1386 glTexCoord2f(1.0f, 0.0f); glVertex2i(w, 0); // top-right
1387 glTexCoord2f(1.0f, 1.0f); glVertex2i(w, h); // bottom-right
1388 glTexCoord2f(0.0f, 1.0f); glVertex2i(0, h); // bottom-left
1389 glEnd();
1392 if (vArrowTextureId) {
1393 if (isMouseVisible) {
1394 int px = oldMouseX/vbufEffScale;
1395 int py = oldMouseY/vbufEffScale;
1396 glEnable(GL_BLEND);
1397 //glBlendFunc(GL_SRC_ALPHA, GL_ONE);
1398 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
1399 glColor4f(1, 1, 1, 1);
1400 glBindTexture(GL_TEXTURE_2D, vArrowTextureId);
1401 //scope(exit) glBindTexture(GL_TEXTURE_2D, 0);
1402 glBegin(GL_QUADS);
1403 glTexCoord2f(0.0f, 0.0f); glVertex2i(px, py); // top-left
1404 glTexCoord2f(1.0f, 0.0f); glVertex2i(px+16, py); // top-right
1405 glTexCoord2f(1.0f, 1.0f); glVertex2i(px+16, py+8); // bottom-right
1406 glTexCoord2f(0.0f, 1.0f); glVertex2i(px, py+8); // bottom-left
1407 glEnd();
1411 glconDraw();
1413 if (isQuitRequested()) vbwin.postEvent(new QuitEvent());
1416 static if (is(typeof(&vbwin.closeQuery))) {
1417 vbwin.closeQuery = delegate () { concmd("quit"); };
1420 vbwin.visibleForTheFirstTime = delegate () {
1421 import iv.glbinds;
1422 vbwin.setAsCurrentOpenGlContext();
1423 vbufEffVSync = vbufVSync;
1424 vbwin.vsync = vbufEffVSync;
1426 // initialize OpenGL texture
1428 enum wrapOpt = GL_REPEAT;
1429 enum filterOpt = GL_NEAREST; //GL_LINEAR;
1430 enum ttype = GL_UNSIGNED_BYTE;
1432 glGenTextures(1, &zxtexid);
1433 if (zxtexid == 0) assert(0, "can't create OpenGL texture");
1435 //GLint gltextbinding;
1436 //glGetIntegerv(GL_TEXTURE_BINDING_2D, &gltextbinding);
1437 //scope(exit) glBindTexture(GL_TEXTURE_2D, gltextbinding);
1439 glBindTexture(GL_TEXTURE_2D, zxtexid);
1440 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapOpt);
1441 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapOpt);
1442 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filterOpt);
1443 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filterOpt);
1444 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
1445 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
1447 GLfloat[4] bclr = 0.0;
1448 glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, bclr.ptr);
1450 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, VBufWidth, VBufHeight, 0, GLTexType, GL_UNSIGNED_BYTE, zxtexbuf.ptr);
1453 createArrowTexture();
1455 glconInit(winWidthScaled, winHeightScaled);
1456 vbwin.redrawOpenGlSceneNow();
1458 updateThreadId = spawn(&updateThread, thisTid);
1461 postScreenUpdate();
1462 repostHideMouse();
1463 foreach (Group g; groups) g.forceUpdated();
1465 vbwin.eventLoop(1000*10,
1466 delegate () {
1467 if (vbwin.closed) return;
1468 if (isQuitRequested) { vbwin.close(); return; }
1469 bool needUpdate = false;
1470 foreach (Group g; groups) if (g.needUpdate()) { needUpdate = true; break; }
1471 if (needUpdate && !updateInProgress) {
1472 updateInProgress = true;
1473 //vbwinLocked = true;
1474 updateThreadId.send(UpThreadCommand.StartUpdate);
1475 //glUpdateTexture();
1476 //vbwin.redrawOpenGlSceneNow();
1478 if (!conQueueEmpty()) postDoConCommands();
1480 delegate (KeyEvent event) {
1481 if (vbwin.closed) return;
1482 if (isQuitRequested) { vbwin.close(); return; }
1483 scope(exit) {
1484 if (!conQueueEmpty()) postDoConCommands();
1486 if (glconKeyEvent(event)) {
1487 postScreenRepaint();
1488 return;
1490 if (subwins.length) {
1491 if (!vbwinLocked || subwins[$-1].immuneToLock) {
1492 if (event.pressed) ignoreSubWinChar = false;
1493 subwins[$-1].onKey(event);
1494 postScreenUpdate();
1495 } else {
1496 ignoreSubWinChar = false;
1498 return;
1500 if (event.pressed) {
1501 if (dbg_dump_keynames) conwriteln("key: ", event.toStr, ": ", event.modifierState&ModifierState.windows);
1502 if (event == "C-L") { concmd("dbg_font_window"); return; }
1503 if (event.pressed && event == "C-Q") { concmd("quit"); return; }
1504 if (event == "N") { concmd("next_unread ona"); return; }
1505 if (event == "S-N") { concmd("next_unread tan"); return; }
1506 if (event == "U") { concmd("mark_unread"); return; }
1507 if (event == "C-R") { concmd("mark_read"); return; }
1508 if (event == "Space") { concmd("artext_page_down"); return; }
1509 if (event == "S-Space") { concmd("artext_page_up"); return; }
1510 if (event == "Up" || event == "Pad8") { concmd("article_prev"); return; }
1511 if (event == "Down" || event == "Pad2") { concmd("article_next"); return; }
1512 if (event == "PageUp" || event == "Pad9") { concmd("article_pgup"); return; }
1513 if (event == "PageDown" || event == "Pad3") { concmd("article_pgdown"); return; }
1514 if (event == "Home" || event == "Pad7") { concmd("article_to_first"); return; }
1515 if (event == "End" || event == "Pad1") { concmd("article_to_last"); return; }
1516 if (event == "C-Up" || event == "C-Pad8") { concmd("article_scroll_up"); return; }
1517 if (event == "C-Down" || event == "C-Pad2") { concmd("article_scroll_down"); return; }
1518 if (event == "C-PageUp" || event == "C-Pad9") { concmd("group_prev"); return; }
1519 if (event == "C-PageDown" || event == "C-Pad3") { concmd("group_next"); return; }
1520 if (event == "C-M-U") { concmd("group_update"); return; }
1521 if (event == "C-H") { concmd("article_dump_headers"); return; }
1522 if (event == "C-S-I") { concmd("update_all"); return; }
1523 if (event == "C-Backslash") { concmd("find_mine"); return; }
1524 if (event == "C-Comma") { concmd("article_to_parent"); return; }
1525 if (event == "C-Insert") {
1526 if (auto g = getActiveGroup) {
1527 g.withBase((abase) {
1528 if (g.curidx < g.length) {
1529 if (auto art = abase[g.baseidx(g.curidx)]) {
1530 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
1531 string id = art.msgid;
1532 if (id.length > 2 && id[0] == '<' && id[$-1] == '>') id = id[1..$-1];
1533 id = "http://forum.dlang.org/post/"~id;
1534 setClipboardText(vbwin, id);
1535 setPrimarySelection(vbwin, id);
1540 return;
1542 if (event == "T") {
1543 if (auto g = getActiveGroup) {
1544 g.withBase(delegate (abase) {
1545 if (g.curidx < g.length) {
1546 if (auto art = abase[g.baseidx(g.curidx)]) {
1547 auto t = twits.check(*art);
1548 if (t) {
1549 new TitlePrompt(art.fromname~" <"~art.frommail~">", t.name, art.msgid, t.title);
1550 } else {
1551 new TitlePrompt(art.fromname~" <"~art.frommail~">", art.fromname~" <"~art.frommail~">", art.msgid, "");
1557 return;
1559 // twit thread
1560 if (event == "C-M-K") {
1561 if (auto g = getActiveGroup) {
1562 g.withBase(delegate (abase) {
1563 if (g.curidx < g.length) {
1564 uint ridx = g.baseidx(g.curidx);
1565 if (auto art = abase[ridx]) {
1566 while (abase[ridx].parent != 0) ridx = abase[ridx].parent;
1567 threadtwits.add(art.msgid);
1568 g.moveToNextThread();
1569 void doMark (uint idx) {
1570 while (idx != 0) {
1571 auto a = abase[idx];
1572 if (a is null) break;
1573 if (a.unread) {
1574 a.unread = false;
1575 a.updated = true;
1577 doMark(a.firstchild);
1578 idx = a.nextsib;
1581 doMark(art.firstchild);
1582 g.buildVisibleList();
1586 postScreenUpdate();
1588 return;
1590 if (event == "R") {
1591 if (auto g = getActiveGroup) {
1592 g.withBase((abase) {
1593 if (g.curidx < g.length) {
1594 abase.loadContent(g.baseidx(g.curidx));
1595 if (auto art = abase[g.baseidx(g.curidx)]) {
1596 new PostEditor(g.groupname, *art);
1601 return;
1603 if (event == "S-P") {
1604 if (auto g = getActiveGroup) {
1605 g.withBase((abase) {
1606 new PostEditor(g.groupname, Article());
1609 return;
1611 if (event == "S-Enter") {
1612 if (auto g = getActiveGroup) {
1613 g.withBase((abase) {
1614 if (g.curidx < g.length) {
1615 if (auto art = abase[g.baseidx(g.curidx)]) {
1616 import std.stdio : File;
1617 import std.process;
1618 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
1619 string id = art.msgid;
1620 if (id.length > 2 && id[0] == '<' && id[$-1] == '>') id = id[1..$-1];
1621 spawnProcess(
1622 ["opera", "http://forum.dlang.org/post/"~id],
1623 File("/dev/zero"), File("/dev/null"), File("/dev/null"),
1629 return;
1632 postScreenRepaint();
1634 delegate (MouseEvent event) {
1635 if (vbwin.closed) return;
1636 scope(exit) {
1637 if (!conQueueEmpty()) postDoConCommands();
1639 oldMouseX = event.x;
1640 oldMouseY = event.y;
1641 mouseMoved();
1642 if (subwins.length) {
1643 auto win = subwins[$-1];
1644 // start drag?
1645 if (!subwinDrag && event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
1646 int mx = event.x/vbufEffScale;
1647 int my = event.y/vbufEffScale;
1648 if (mx >= win.winx && my >= win.winy && mx < win.winx+win.winw && my < win.winy+10) {
1649 subwinDrag = true;
1650 subwinDragXSpot = win.winx-mx;
1651 subwinDragYSpot = win.winy-my;
1652 postScreenUpdate();
1653 return;
1656 // draw
1657 if (subwinDrag) {
1658 win.winx = event.x/vbufEffScale+subwinDragXSpot;
1659 win.winy = event.y/vbufEffScale+subwinDragYSpot;
1661 // stop drag?
1662 if (subwinDrag && event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
1663 subwinDrag = false;
1664 postScreenUpdate();
1665 return;
1667 if (!vbwinLocked || win.immuneToLock) win.onMouse(event);
1668 postScreenUpdate();
1669 return;
1671 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
1672 int mx = event.x/vbufEffScale;
1673 int my = event.y/vbufEffScale;
1674 // select group
1675 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < groups.length*(gxTextHeightUtf+2)) {
1676 if (auto g = getActiveGroup) g.active = false;
1677 groups[my/(gxTextHeightUtf+2)].active = true;
1678 postScreenUpdate();
1679 return;
1681 // select post
1682 if (mx > guiGroupListWidth && mx < VBufWidth && my >= 0 && my < guiThreadListHeight) {
1683 if (auto g = getActiveGroup) {
1684 my -= g.ytopofs;
1685 my /= gxTextHeightUtf;
1686 g.curidx = g.msgtop+my;
1687 postScreenUpdate();
1688 return;
1692 // wheel
1693 if (event.type == MouseEventType.buttonPressed && (event.button == MouseButton.wheelUp || event.button == MouseButton.wheelDown)) {
1694 int mx = event.x/vbufEffScale;
1695 int my = event.y/vbufEffScale;
1696 // group
1697 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < VBufHeight) {
1698 if (groups.length > 0) {
1699 int gi = getActiveGroupIndex();
1700 gi += (event.button == MouseButton.wheelUp ? -1 : 1);
1701 if (gi < 0) gi = 0;
1702 if (gi >= groups.length) gi = cast(int)groups.length-1;
1703 if (auto g = getActiveGroup) g.active = false;
1704 groups[gi].active = true;
1705 postScreenUpdate();
1707 return;
1709 // select post
1710 if (mx > guiGroupListWidth && mx < VBufWidth && my >= 0 && my < guiThreadListHeight) {
1711 if (auto g = getActiveGroup) {
1712 if (event.button == MouseButton.wheelUp) g.moveUp(); else g.moveDown();
1713 postScreenUpdate();
1714 return;
1718 postScreenRepaint();
1720 delegate (dchar ch) {
1721 if (vbwin.closed) return;
1722 scope(exit) {
1723 if (!conQueueEmpty()) postDoConCommands();
1725 if (glconCharEvent(ch)) {
1726 postScreenRepaint();
1727 return;
1729 if (subwins.length) {
1730 if (!vbwinLocked || subwins[$-1].immuneToLock) {
1731 if (!ignoreSubWinChar) subwins[$-1].onChar(ch);
1732 ignoreSubWinChar = false;
1733 postScreenUpdate();
1734 } else {
1735 ignoreSubWinChar = false;
1737 return;
1741 updateThreadId.send(UpThreadCommand.Quit);