"C-Period" to find my messages; "C-Insert" to copy post URL to clipboard
[knntp.git] / nntpreader.d
blob82a28c1f5ce42b8bed456af7659656d0b06c2ea9
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.simpledisplay;
26 import iv.bclamp;
27 import iv.encoding;
28 import iv.cmdcon;
29 import iv.cmdcongl;
30 import iv.strex;
31 import iv.utfutil;
32 import iv.vfs;
34 import egfx;
35 import nntp;
36 import editor;
37 import twitlist;
38 import group;
39 import egui;
42 // ////////////////////////////////////////////////////////////////////////// //
43 __gshared int oldMouseX, oldMouseY;
44 __gshared ubyte oldMouseButtons;
47 // ////////////////////////////////////////////////////////////////////////// //
48 __gshared Tid updateThreadId;
51 // ////////////////////////////////////////////////////////////////////////// //
52 enum UpThreadCommand {
53 Ping,
54 StartUpdate, // start updating now
55 Quit,
58 void updateThread (Tid ownerTid) {
59 bool doQuit = false;
60 bool doUpdates = false;
61 try {
62 while (!doQuit) {
63 receive(
64 (UpThreadCommand cmd) {
65 final switch (cmd) {
66 case UpThreadCommand.Ping: break;
67 case UpThreadCommand.StartUpdate: doUpdates = true; break;
68 case UpThreadCommand.Quit: doQuit = true; break;
72 if (doQuit) break;
73 if (doUpdates) {
74 doUpdates = false;
75 foreach (immutable gidx, Group g; groups) {
76 if (g.needUpdate) {
77 if (vbwin !is null) vbwin.postEvent(new UpdatingGroupEvent(cast(uint)gidx));
78 g.doUpdate();
79 if (vbwin !is null) vbwin.postEvent(new UpdatingGroupCompleteEvent(cast(uint)gidx));
80 //conwriteln("group '", g.mbase.groupname, "' update complete");
83 //conwriteln("all groups updated");
84 if (vbwin !is null) {
85 vbwin.postEvent(new UpdatingCompleteEvent());
86 //conwriteln("complete event sent");
90 } catch (Throwable e) {
91 // here, we are dead and fucked (the exact order doesn't matter)
92 import core.stdc.stdlib : abort;
93 import core.stdc.stdio : fprintf, stderr;
94 import core.memory : GC;
95 import core.thread : thread_suspendAll;
96 GC.disable(); // yeah
97 thread_suspendAll(); // stop right here, you criminal scum!
98 auto s = e.toString();
99 fprintf(stderr, "\n=== FATAL ===\n%.*s\n", cast(uint)s.length, s.ptr);
100 abort(); // die, you bitch!
105 // ////////////////////////////////////////////////////////////////////////// //
106 void initConsole () {
107 conRegVar!VBufScale(1, 4, "v_scale", "window scale: [1..3]");
109 conRegVar!bool("v_vsync", "sync to video refresh rate?",
110 (ConVarBase self) => vbufVSync,
111 (ConVarBase self, bool nv) {
112 if (vbufVSync != nv) {
113 vbufVSync = nv;
114 postScreenRepaint();
119 conRegFunc!(() {
120 if (groups.length == 0) return;
121 auto gidx = getActiveGroupIndex();
122 if (gidx == 0) return;
123 if (gidx >= 0) groups[gidx].deactivate();
124 gidx = (gidx < 0 ? 0 : gidx-1);
125 assert(gidx >= 0 && gidx < groups.length);
126 groups[gidx].activate();
127 postScreenUpdate();
128 })("group_prev", "go to previous group");
130 conRegFunc!(() {
131 if (groups.length == 0) return;
132 auto gidx = getActiveGroupIndex();
133 if (gidx == groups.length-1) return;
134 if (gidx >= 0) groups[gidx].deactivate();
135 gidx = (gidx < 0 ? 0 : gidx+1);
136 assert(gidx >= 0 && gidx < groups.length);
137 groups[gidx].activate();
138 postScreenUpdate();
139 })("group_next", "go to next group");
141 conRegFunc!(() {
142 foreach (immutable gidx, Group g; groups) {
143 if (g.active) {
144 if (g.markAsUnread) postScreenUpdate();
145 return;
148 })("mark_unread", "mark current message as unread");
150 conRegFunc!(() {
151 foreach (immutable gidx, Group g; groups) {
152 if (g.active) {
153 if (g.markAsRead) postScreenUpdate();
154 return;
157 })("mark_read", "mark current message as read");
159 conRegFunc!((bool allowNextGroup=false) {
160 foreach (immutable _; 0..groups.length) {
161 uint actgidx = uint.max;
162 foreach (immutable gidx, Group g; groups) {
163 if (!g.active) continue;
164 actgidx = cast(uint)gidx;
165 if (g.moveToNextUnread) {
166 postScreenUpdate();
167 return;
170 if (!allowNextGroup) break;
171 // move to next group
172 if (actgidx == uint.max) {
173 actgidx = 0;
174 } else {
175 groups[actgidx].releaseContent();
176 groups[actgidx].active = false;
177 actgidx = (actgidx+1)%groups.length;
179 groups[actgidx].active = true;
181 })("next_unread", "move to next unread message; bool arg: can skip to next group?");
183 conRegFunc!(() {
184 if (articleTextTopLine > 0) {
185 articleTextTopLine -= (VBufHeight-guiThreadListHeight-3*10-4)/gxTextHeightUtf;
186 if (articleTextTopLine < 0) articleTextTopLine = 0;
187 postScreenUpdate();
189 })("artext_page_up", "do pageup on article text");
191 conRegFunc!(() {
192 articleTextTopLine += (VBufHeight-guiThreadListHeight-3*10-4)/gxTextHeightUtf;
193 postScreenUpdate();
194 })("artext_page_down", "do pagedown on article text");
196 conRegFunc!(() {
197 if (auto g = getActiveGroup) {
198 if (g.moveUp) postScreenUpdate();
200 })("article_prev", "go to previous article");
202 conRegFunc!(() {
203 if (auto g = getActiveGroup) {
204 if (g.moveDown) postScreenUpdate();
206 })("article_next", "go to next article");
208 conRegFunc!(() {
209 if (auto g = getActiveGroup) {
210 if (g.movePageUp) postScreenUpdate();
212 })("article_pgup", "artiles list: page up");
214 conRegFunc!(() {
215 if (auto g = getActiveGroup) {
216 if (g.movePageDown) postScreenUpdate();
218 })("article_pgdown", "artiles list: page down");
220 conRegFunc!(() {
221 if (auto g = getActiveGroup) {
222 if (g.scrollUp) postScreenUpdate();
224 })("article_scroll_up", "scroll article list up");
226 conRegFunc!(() {
227 if (auto g = getActiveGroup) {
228 if (g.scrollDown) postScreenUpdate();
230 })("article_scroll_down", "scroll article list up");
232 conRegFunc!(() {
233 if (auto g = getActiveGroup) {
234 if (g.moveToFirst) postScreenUpdate();
236 })("article_to_first", "go to first article");
238 conRegFunc!(() {
239 if (auto g = getActiveGroup) {
240 if (g.moveToLast) postScreenUpdate();
242 })("article_to_last", "go to last article");
244 conRegFunc!(() {
245 foreach (Group g; groups) g.forceUpdating();
246 })("update_all", "mark all groups for updating");
249 // //////////////////////////////////////////////////////////////////// //
250 // debug
251 conRegFunc!(() {
252 if (auto g = getActiveGroup) {
253 static import iv.vfs.io;
255 g.withBase(delegate (abase) {
256 auto sk = new SocketNNTP("news.digitalmars.com");
257 scope(exit) {
258 if (sk.active) sk.quit();
259 sk.close();
262 sk.selectGroup(abase.groupname);
264 if (sk.emptyGroup) return;
266 uint stnum = abase.maxnum+1;
268 if (abase.length == 0) stnum = (sk.hiwater > 1023 ? sk.hiwater-1023 : 0);
269 if (stnum > sk.hiwater) { conwriteln("no new articles"); return; }
271 conwriteln(sk.hiwater+1-stnum, " new articles");
273 // download new articles
274 foreach (immutable uint anum; stnum..sk.hiwater+1) {
275 import std.conv : to;
276 iv.vfs.io.write("\r[", anum, "/", sk.hiwater, "] ... \x1b[K");
277 auto art = sk.getArticle(anum);
278 if (!art.valid) { iv.vfs.io.writeln("SKIP"); continue; }
279 iv.vfs.io.write("OK");
280 art.flags |= Article.Flag.Unread;
281 abase.insert(art);
282 //abase.selfCheck();
284 iv.vfs.io.writeln;
286 abase.selfCheck();
287 abase.writeUpdates();
289 g.buildVisibleList();
292 postScreenUpdate();
294 })("group_update", "update current group");
296 conRegFunc!(() {
297 if (auto g = getActiveGroup) {
298 if (g.curidxValid) {
299 g.withBase(delegate (abase) {
300 auto art = abase[g.baseidx(g.curidx)];
301 if (art !is null) {
302 conwriteln("============================");
304 conwriteln("replyto: ", art.replyto, "|");
305 if (art.replyto.length) {
306 auto arr = g.mbase[art.replyto];
307 if (arr !is null) conwriteln("*** ", arr.from, " ***");
310 foreach (string s; art.headers) conwriteln(" ", s);
311 conwriteln("---------------");
312 conwriteln(" ", art.fromname, " <", art.frommail, ">");
313 conwrite(" ");
314 Utf8DecoderFast dc;
315 foreach (char ch; art.fromname) {
316 if (dc.decode(cast(ubyte)ch)) {
317 if (dc.codepoint > 127 || dc.codepoint < 32) conwritef!" \\u%04X "(cast(uint)dc.codepoint);
318 else conwrite(cast(char)dc.codepoint);
321 conwriteln();
326 })("article_dump_headers", "dump article headers");
328 conRegFunc!(() {
329 if (auto g = getActiveGroup) {
330 g.withBase(delegate (abase) {
331 uint idx = g.curidx;
332 if (idx >= g.length) {
333 idx = 0;
334 } else if (auto art = abase[g.baseidx(idx)]) {
335 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
336 idx = (idx+1)%g.length;
339 foreach (immutable _; 0..g.length) {
340 auto art = abase[g.baseidx(idx)];
341 if (art !is null) {
342 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
343 g.curidx = cast(int)idx;
344 postScreenUpdate();
345 return;
348 idx = (idx+1)%g.length;
352 })("find_mine", "find mine article");
356 // ////////////////////////////////////////////////////////////////////////// //
357 void glUpdateTexture () {
358 import iv.glbinds;
360 zxtexbuf[] = 0;
361 clipReset();
363 gxFillRect(0, 0, guiGroupListWidth, VBufHeight, gxRGB!(20, 20, 20));
364 gxVLine(guiGroupListWidth, 0, VBufHeight, gxRGB!(255, 255, 255));
366 gxFillRect(guiGroupListWidth+1, 0, VBufWidth, guiThreadListHeight, gxRGB!(15, 15, 15));
367 gxHLine(guiGroupListWidth+1, guiThreadListHeight, VBufWidth, gxRGB!(255, 255, 255));
369 void drawArticle (Group g, in ref Article art, string title) {
370 import std.format : format;
371 import std.datetime;
373 if (!art.valid || !art.contentLoaded) {
374 lastArticleText.clear();
375 articleTextTopLine = 0;
378 if (!lastArticleText.equal(g, art)) {
379 lastArticleText.set(g, art);
380 articleTextTopLine = 0;
383 clipX0 = guiGroupListWidth+2;
384 clipX1 = VBufWidth-1;
385 clipY0 = guiThreadListHeight+1;
386 clipY1 = VBufHeight-1;
388 gxFillRect(clipX0, clipY0, clipX1-clipX0+1, 3*gxTextHeightUtf+2, gxRGB!(30, 30, 30));
389 gxDrawTextUtf(clipX0+1, clipY0+0*gxTextHeightUtf+1, "From: %s <%s>".format(art.fromname, art.frommail), gxRGB!(0, 128, 128));
390 gxDrawTextUtf(clipX0+1, clipY0+1*gxTextHeightUtf+1, "Subject: %s".format(art.subj), gxRGB!(0, 128, 128));
391 auto t = SysTime.fromUnixTime(art.time);
392 string s = "Date: %04d/%02d/%02d %02d:%02d:%02d".format(t.year, t.month, t.day, t.hour, t.minute, t.second);
393 gxDrawTextUtf(clipX0+1, clipY0+2*gxTextHeightUtf+1, s, gxRGB!(0, 128, 128));
395 //TODO: text reflow
396 int y = clipY0+3*gxTextHeightUtf+2;
397 immutable sty = y;
399 int lines = (clipY1-y)/gxTextHeightUtf;
400 if (lines < 1 || art.text.length <= lines) {
401 articleTextTopLine = 0;
402 } else {
403 if (articleTextTopLine+lines > art.text.length) {
404 articleTextTopLine = cast(int)art.text.length-lines;
405 if (articleTextTopLine < 0) articleTextTopLine = 0;
409 uint idx = articleTextTopLine;
410 while (idx < art.text.length && y < VBufHeight) {
411 int qlevel = 0;
412 s = art.text[idx];
413 uint clr = gxRGB!(0, 128, 0);
415 foreach (char ch; s) {
416 if (ch <= ' ') continue;
417 if (ch != '>') break;
418 ++qlevel;
421 clr = gxRGB!( 0, 128+40, 0);
422 if (qlevel) {
423 final switch (qlevel%2) {
424 case 0: clr = gxRGB!(128, 128, 0); break;
425 case 1: clr = gxRGB!( 0, 128, 128); break;
429 /*if (qlevel == 0 && s.length && s[0] <= ' ') {
430 gxDrawText(clipX0+1, y, s, clr);
431 } else*/ {
432 gxDrawTextUtf(clipX0+1, y, s, clr);
435 if (clipY1-y < gxTextHeightUtf && art.text.length-idx > 0) {
436 // draw "down" indicator
437 gxDrawTextOutP(clipX1-gxTextWidthP(triangleUpStr)-3, clipY1-7, triangleUpStr, gxRGB!(255, 255, 255), gxRGB!(0, 69, 69));
440 ++idx;
441 y += gxTextHeightUtf;
444 if (articleTextTopLine > 0) {
445 gxDrawTextOutP(clipX1-gxTextWidthP(triangleDownStr)-3, sty, triangleDownStr, gxRGB!(255, 255, 255), gxRGB!(0, 69, 69));
448 if (title.length) {
449 foreach (immutable dy; clipY0+3*gxTextHeightUtf+2..clipY1+1) {
450 foreach (immutable dx; clipX0..clipX1+1) {
451 if ((dx^dy)&1) gxPutPixel(dx, dy, gxRGB!(0, 0, 80));
455 int tx = clipX0+(clipWidth-gxTextWidthScaledUtf(3, title))/2-1;
456 int ty = clipY0+(clipHeight-3*gxTextHeightUtf)/2-1;
457 foreach (immutable dy; -1..1) {
458 foreach (immutable dx; -1..1) {
459 gxDrawTextScaledUtf(3, tx+dx, ty+dy, title, 0);
462 gxDrawTextScaledUtf(3, tx, ty, title, gxRGB!(255, 0, 0));
466 void drawThreadList (Group g) {
467 g.withBase(delegate (abase) {
468 g.makeCurrentVisible();
470 clipX0 = guiGroupListWidth+2;
471 clipX1 = VBufWidth-1-4;
472 clipY0 = 0;
473 clipY1 = guiThreadListHeight-1;
474 immutable uint origX0 = clipX0;
475 immutable uint origX1 = clipX1;
476 immutable uint origY0 = clipY0;
477 immutable uint origY1 = clipY1;
478 int y = -g.ytopofs;
479 //conwriteln(g.msgtop, " : ", g.list.length);
480 uint idx = g.msgtop;
481 while (idx < g.length && y < guiThreadListHeight) {
482 import std.format : format;
483 import std.datetime;
485 if (y >= guiThreadListHeight) break;
486 if (idx >= g.length) break;
488 clipX0 = origX0;
489 clipX1 = origX1;
491 //conwriteln(idx, " : ", g.list.length);
492 if (idx == g.curidx) gxFillRect(clipX0, y, clipX1-clipX0+1, gxTextHeightUtf, gxRGB!(0, 127, 127));
493 ++clipX0;
494 --clipX1;
496 auto art = abase[g.baseidx(idx)];
498 //uint clr = (idx != g.curidx ? gxRGB!(255, 127, 0) : gxRGB!(255, 255, 255));
499 uint clr = (art.unread ? gxRGB!(255, 255, 255) : gxRGB!(255-60, 127-60, 0));
500 uint clr1 = (art.unread ? gxRGB!(255, 255, 0) : gxRGB!(255-60-40, 127-60-40, 0));
502 if (g.twited(idx).length) { clr = gxRGB!(60, 0, 0); clr1 = gxRGB!(60, 0, 0); }
503 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) { clr = gxRGB!(0, 190, 0); clr1 = gxRGB!(0, 190-40, 0); }
505 auto t = SysTime.fromUnixTime(art.time);
506 //string s = "%04d/%02d/%02d %02d:%02d".format(t.year, t.month, t.day, t.hour, t.minute);
508 import core.stdc.stdio : snprintf;
509 char[128] tmpbuf;
510 //string s = "%02d/%02d %02d:%02d".format(t.month, t.day, t.hour, t.minute);
511 auto len = snprintf(tmpbuf.ptr, tmpbuf.length, "%02d/%02d/%02d %02d:%02d", t.year%100, t.month, t.day, t.hour, t.minute);
512 gxDrawTextUtf(clipX1-gxTextWidthUtf(tmpbuf[0..len]), y, tmpbuf[0..len], clr);
515 string from = art.fromname;
516 if (from.length > 2 && from[0] == '"' && from[$-1] == '"') {
517 from = from[1..$-1].xstrip;
518 if (from.length == 0) from = "anonymous";
521 auto vp = from.indexOf(" via Digitalmars-");
522 if (vp > 0) {
523 from = from[0..vp].xstrip;
524 if (from.length == 0) from = "anonymous";
528 clipX1 -= 13*6+4;
529 gxDrawTextUtf(clipX1-22*6, y, from, clr);
530 gxDrawTextUtf(clipX1-22*6+gxTextWidthUtf(from)+4, y, "<", clr1);
531 gxDrawTextUtf(clipX1-22*6+gxTextWidthUtf(from)+4+gxTextWidthUtf("<")+1, y, art.frommail, clr1);
532 gxDrawTextUtf(clipX1-22*6+gxTextWidthUtf(from)+4+gxTextWidthUtf("<")+1+gxTextWidthUtf(art.frommail)+1, y, ">", clr1);
534 clipX1 -= 22*6+4;
535 gxDrawTextUtf(clipX0+art.depth*3, y, art.subj, clr);
537 ++idx;
538 y += gxTextHeightUtf;
541 // draw progressbar
543 //if (idx > g.list.length) idx = cast(uint)g.list.length;
544 clipX0 = origX0;
545 clipX1 = origX1+4;
546 clipY0 = origY0;
547 clipY1 = origY1;
548 int hgt = clipY1-clipY0+1-4;
549 int pix = cast(int)(cast(long)hgt*idx/g.length);
550 if (pix > hgt) pix = hgt;
551 gxVLine(clipX1-2, clipY0+2, pix, gxRGB!(160, 160, 160));
552 // frame
553 gxVLine(clipX1-3, clipY0+2, hgt, gxRGB!(220, 220, 220));
554 gxVLine(clipX1-1, clipY0+2, hgt, gxRGB!(220, 220, 220));
555 gxHLine(clipX1-2, clipY0+1, 1, gxRGB!(220, 220, 220));
556 gxHLine(clipX1-2, clipY1-1, 1, gxRGB!(220, 220, 220));
559 if (g.curidx < g.length) {
560 abase.loadContent(g.baseidx(g.curidx));
561 drawArticle(g, *abase[g.baseidx(g.curidx)], g.twited(g.curidx));
566 foreach (immutable idx, Group g; groups) {
567 int ofsx = 2;
568 int ofsy = 1+cast(int)idx*(gxTextHeightUtf+2);
569 clipReset();
570 if (g.active) gxFillRect(0, ofsy-1, guiGroupListWidth, (gxTextHeightUtf+2), gxRGB!(0, 127, 127));
571 clipX0 = ofsx-1;
572 clipY0 = ofsy;
573 clipX1 = guiGroupListWidth-3;
574 uint clr = (g.unreadCount ? gxRGB!(255, 255, 0) : gxRGB!(255, 127, 0));
575 if (g.uiFlagUpdating) clr = gxRGB!(0, 255, 255);
576 gxDrawTextOutP(ofsx, ofsy, g.groupname, clr, gxRGB!(0, 0, 0));
577 if (g.active) {
578 drawThreadList(g);
582 foreach (immutable idx, SubWindow w; subwins) {
583 if (idx == subwins.length-1 && w.immuneToLock) break;
584 w.onPaint();
587 if (vbwinLocked) {
588 clipReset();
589 foreach (immutable y; 0..VBufHeight) {
590 foreach (immutable x; 0..VBufWidth) {
591 if ((x^y)&1) gxPutPixel(x, y, gxRGB!(0, 0, 99));
596 if (subwins.length && subwins[$-1].immuneToLock) subwins[$-1].onPaint();
598 if (zxtexid) {
599 glBindTexture(GL_TEXTURE_2D, zxtexid);
600 glTexSubImage2D(GL_TEXTURE_2D, 0, 0/*x*/, 0/*y*/, VBufWidth, VBufHeight, GLTexType, GL_UNSIGNED_BYTE, zxtexbuf.ptr);
601 //glBindTexture(GL_TEXTURE_2D, 0);
606 // ////////////////////////////////////////////////////////////////////////// //
607 class TitlePrompt : SubWindow {
608 string fullname;
609 string name;
610 string msgid;
611 LineEdit etitle;
613 @property string title () const pure nothrow @safe @nogc { return etitle.str; }
615 static __gshared int titleCount = 0;
617 this (string afullname, string aname, string amsgid, string atitle=null) {
618 super(260, 28);
619 if (titleCount != 0) return;
620 fullname = afullname;
621 name = aname;
622 msgid = amsgid;
623 etitle = new LineEdit();
624 etitle.str = atitle;
625 etitle.active = true;
626 immuneToLock = true;
627 add();
630 override void close () {
631 if (titleCount > 0) --titleCount;
632 super.close();
635 override void onPaint () {
636 setupClip();
637 gxDrawWindow(name, gxRGB!(255, 255, 255), gxRGB!(0, 0, 0), gxRGB!(0, 0, 180));
639 setupClientClip();
640 clipX0 += 4;
641 clipX1 -= 4;
642 clipY0 += 2;
643 etitle.onPaint();
646 override void onKey (KeyEvent event) {
647 if (!event.pressed) return;
648 if (event == "Escape") { close(); return; }
649 if (event == "Enter") {
650 twits.update(name, msgid, title, fullname);
651 if (!vbwinLocked) {
652 if (auto g = getActiveGroup) {
653 g.withBase(delegate (abase) {
654 if (g.curidx < g.length && abase[g.baseidx(g.curidx)].depth == 0) {
655 g.moveToNextThread();
657 g.buildVisibleList();
661 close();
662 return;
664 etitle.onKey(event);
667 override void onMouse (MouseEvent event) {
670 override void onChar (dchar ch) {
671 etitle.onChar(ch);
676 // ////////////////////////////////////////////////////////////////////////// //
677 class PostEditor : SubWindow {
678 static __gshared int editorCount = 0;
680 string groupname;
681 string replyid; // replyto
682 string[] hdrs;
683 Editor editor;
684 int topline;
685 LineEdit esubj;
686 bool subjActive;
688 @property string subj () const pure nothrow @safe @nogc { return esubj.str; }
690 bool doSend () {
691 if (subj.length == 0) return false;
693 try {
694 import std.digest.sha;
695 import std.datetime;
696 import std.uuid;
698 conwriteln("sending to '", groupname, "'");
700 // build references
701 string[] refs;
702 if (replyid.length) {
703 void addRef (string s) {
704 s = s.xstrip;
705 if (s.length == 0) return;
706 if (refs.length == 0) refs ~= "References:";
707 if (refs.length+s.length > 72) {
708 refs ~= "\t"~s;
709 } else {
710 refs[$-1] ~= " "~s;
713 void procRefs (string s) {
714 for (;;) {
715 s = s.xstrip;
716 if (s.length == 0) break;
717 usize pos = 0;
718 while (pos < s.length && s[pos] > ' ') ++pos;
719 assert(pos > 0);
720 addRef(s[0..pos]);
721 s = s[pos..$];
724 string preplyto;
725 bool lastWasRef = false;
726 foreach (string hss; hdrs) {
727 if (hss.startsWithCI("References:")) {
728 procRefs(hss[11..$]);
729 lastWasRef = true;
730 } else if (hss[0] <= ' ') {
731 if (lastWasRef) procRefs(hss);
732 } else {
733 lastWasRef = false;
734 if (preplyto.length == 0 && hss.startsWithCI("In-Reply-To:")) {
735 preplyto = hss[12..$].xstrip;
739 if (refs.length == 0 && preplyto.length) procRefs(preplyto);
740 addRef(replyid);
741 conwriteln("============== REFS ==============");
742 foreach (string s; refs) conwriteln(" |", s, "|");
743 conwriteln("-----------------------");
747 SHA256 hash;
748 hash.put(cast(const(ubyte)[])Clock.currTime().toString);
749 hash.put(cast(const(ubyte)[])groupname);
750 hash.put(cast(const(ubyte)[])replyid);
751 hash.put(cast(const(ubyte)[])subj);
752 foreach (immutable idx; 0..editor.lineCount) hash.put(cast(const(ubyte)[])editor[idx]);
753 auto hashres = hash.finish();
754 string hashstr = toHexString!(LetterCase.lower)(hashres);
756 UUID id = randomUUID();
757 string hashstr = toHexString!(LetterCase.lower)(id.data);
758 hashstr = "<"~hashstr~"@dingo>";
759 conwriteln(" message id: ", hashstr);
760 conwriteln(" reply to : ", replyid);
762 SocketNNTP sk;
763 try {
764 sk = new SocketNNTP("news.digitalmars.com");
765 } catch (Exception e) {
766 conwriteln("connection error: ", e.msg);
767 return false;
769 scope(exit) {
770 if (sk.active) sk.quit();
771 sk.close();
774 sk.selectGroup(groupname);
776 sk.doSend("%s", "POST");
777 sk.doSend("%s", "From: ketmar <ketmar@ketmar.no-ip.org>");
778 sk.doSend("Newsgroups: %s", groupname);
779 sk.doSend("Subject: %s", subj);
780 sk.doSend("Message-ID: %s", hashstr);
781 sk.doSend("%s", "Mime-Version: 1.0");
782 sk.doSend("%s", "Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no");
783 sk.doSend("%s", "Content-Transfer-Encoding: 8bit");
784 sk.doSend("%s", "User-Agent: dingo");
785 foreach (string s; refs) sk.doSend("%s", s);
786 if (replyid.length) sk.doSend("In-Reply-To: %s", replyid);
787 sk.doSend("%s", "");
788 foreach (immutable idx; 0..editor.lineCount) {
789 string s = editor[idx];
790 if (s.length > 0 && s[0] == '.') s = "."~s;
791 sk.doSend("%s", s);
793 sk.doSend("%s", ".");
795 auto ln = sk.readLine;
796 conwriteln(ln); // 340 Ok, recommended message-ID <o7dq4o$mpm$1@digitalmars.com>
798 if (ln.length == 0 || ln[0] != '3') throw new Exception(ln.idup);
800 foreach (Group g; groups) {
801 if (g.groupname == groupname) g.forceUpdating();
804 return true;
805 } catch (Exception e) {
806 conwriteln("SENDING ERROR: ", e.msg);
809 return false;
812 this() (string agroupname, in auto ref Article art) {
813 super(506, 253);
814 if (editorCount != 0) return;
815 if (agroupname.length == 0) return;
816 editor = new Editor();
817 esubj = new LineEdit();
818 if (art.valid) {
819 esubj.str = "Re: "~art.subj;
820 replyid = art.msgid;
821 hdrs = art.headers.dup;
823 if (art.valid && art.contentLoaded) {
824 editor.addLine(art.fromname~" wrote:");
825 //editor.addLine("");
826 foreach (string s; art.text) {
827 editor.addLine(">"~s);
829 editor.addLine("");
830 editor.reformat();
832 if (editor.lineCount == 0) editor.addLine("");
833 immuneToLock = true;
834 groupname = agroupname;
835 add();
838 override void close () {
839 if (editorCount > 0) --editorCount;
840 super.close();
843 final void textClip () {
844 setupClientClip();
845 clipX0 += 1;
846 clipX1 -= 1+4;
847 clipY0 += 12+6;
848 clipY1 -= 1;
851 final void drawSubj () {
852 setupClientClip(); // the easiest way again
853 //gxFillRect(clipX0, clipY0, clipWidth, 14, gxRGB!(0, 0, 120));
854 clipY0 += 1;
855 gxFillRect(clipX0+2, clipY0, clipWidth-4, 10, (subjActive ? gxRGB!(0, 0, 0) : gxRGB!(20, 20, 20)));
856 clipX0 += 4;
857 clipX1 -= 4;
858 clipY0 += 1;
859 esubj.active = subjActive;
860 esubj.onPaint();
863 final void makeCurVisible () {
864 textClip(); // the easiest way to get the size
865 int lvis = clipHeight/gxTextHeightUtf;
866 if (lvis < 1) lvis = 1; // just in case
867 int ltop = topline;
868 int lbot = topline+lvis-1;
869 int cy = editor.cury;
870 if (cy < ltop) {
871 topline = cy;
872 } else if (cy > lbot) {
873 topline = cy-lvis+1;
874 if (topline < 0) topline = 0;
878 final void drawScrollBar () {
879 textClip(); // the easiest way again
880 clipX1 += 4;
881 // frame
882 gxVLine(clipX1-2, clipY0+1, clipHeight-2, gxRGB!(220, 220, 220));
883 gxVLine(clipX1-0, clipY0+1, clipHeight-2, gxRGB!(220, 220, 220));
884 gxHLine(clipX1-1, clipY0, 1, gxRGB!(220, 220, 220));
885 gxHLine(clipX1-1, clipY1, 1, gxRGB!(220, 220, 220));
886 int pix = (clipHeight-2)*editor.cury/editor.lineCount;
887 if (pix > clipHeight-2) pix = clipHeight-2;
888 gxVLine(clipX1-1, clipY0+1, pix, gxRGB!(160, 160, 160));
891 override void onPaint () {
892 setupClip();
894 gxDrawWindow(groupname, gxRGB!(255, 255, 255), gxRGB!(0, 0, 0), gxRGB!(0, 0, 180));
896 textClip();
898 makeCurVisible();
900 int lidx = topline;
901 int y = clipY0;
902 while (lidx < editor.lineCount && y <= clipY1) {
903 string s = editor[lidx];
904 int qlevel = editor.quoteLevel(lidx);
905 uint clr = gxRGB!(220, 220, 0);
906 uint markBgClr = gxRGB!(227, 0, 195);
907 if (qlevel) {
908 final switch (qlevel%2) {
909 case 0: clr = gxRGB!(128, 128, 0); break;
910 case 1: clr = gxRGB!( 0, 128, 128); break;
913 if (!editor.lineHasMark(lidx)) {
914 gxDrawTextUtf(clipX0, y, s, clr);
915 } else {
916 int xx = clipX0;
917 int cx = 0;
918 utfByLocal(editor[lidx], delegate (char ch) {
919 int w = gxCharWidthP(ch)+1;
920 if (editor.isMarked(cx, lidx)) {
921 gxFillRect(xx, y, w-(editor.isMarked(cx+1, lidx) ? 0 : 1), gxTextHeightUtf, markBgClr);
923 gxDrawCharP(xx, y, ch, clr);
924 xx += w;
925 ++cx;
928 if (!subjActive && lidx == editor.cury) {
929 int xpos = gxTextWidthUtf(s.utfleft(editor.curx));
930 drawTextCursor(clipX0+xpos, y);
932 ++lidx;
933 y += gxTextHeightUtf;
935 if (!subjActive && editor.cury >= editor.lineCount) drawTextCursor(clipX0, y);
936 drawScrollBar();
937 drawSubj();
938 if (!subjActive) postCurBlink();
941 override void onKey (KeyEvent event) {
942 if (!event.pressed) return;
943 if (event == "C-Escape" || event == "M-Escape" || event == "C-C" || event == "C-Q") { close(); return; }
944 if (event == "C-Enter") {
945 if (!vbwinLocked) {
946 if (doSend()) close();
948 return;
950 if (event == "Tab") { subjActive = !subjActive; return; }
951 if (!subjActive) {
952 if (event == "Escape") { editor.putChar(editor.SpecCh.ResetMark); return; }
953 if (event == "C-Space") { editor.putChar(editor.SpecCh.PutMark); return; }
954 if (event == "Enter" || event == "PadEnter") { editor.putChar(editor.SpecCh.Enter); return; }
955 if (event == "Left" || event == "Pad4") { editor.putChar(editor.SpecCh.Left); return; }
956 if (event == "Right" || event == "Pad6") { editor.putChar(editor.SpecCh.Right); return; }
957 if (event == "Up" || event == "Pad8") { editor.putChar(editor.SpecCh.Up); return; }
958 if (event == "Down" || event == "Pad2") { editor.putChar(editor.SpecCh.Down); return; }
959 if (event == "Delete" || event == "PadDot") { editor.putChar(editor.SpecCh.Delete); return; }
960 if (event == "Home" || event == "Pad7") { editor.putChar(editor.SpecCh.Home); return; }
961 if (event == "End" || event == "Pad1") { editor.putChar(editor.SpecCh.End); return; }
962 if (event == "C-Y") { editor.putChar(editor.SpecCh.KillLine); return; }
963 if (event == "S-Insert") {
964 getClipboardText(vbwin, delegate (in char[] text) {
965 if (!closed) editor.putUtf(text[]);
967 return;
969 if (event == "C-Insert") {
970 string ct = editor.getSelectionText();
971 if (ct.length > 0) {
972 setClipboardText(vbwin, ct);
973 setPrimarySelection(vbwin, ct);
975 editor.putChar(editor.SpecCh.ResetMark);
976 return;
978 } else {
979 esubj.onKey(event);
983 override void onMouse (MouseEvent event) {
986 override void onChar (dchar ch) {
987 if (!subjActive) {
988 if (ch == 8) { editor.putChar(ch); return; }
989 if (ch < ' ' || ch == 127) return;
990 editor.putChar(ch);
991 } else {
992 esubj.onChar(ch);
998 // ////////////////////////////////////////////////////////////////////////// //
999 void main (string[] args) {
1000 sdpyWindowClass = "NNTPReaderDingo";
1001 glconShowKey = "C-Grave";
1003 initConsole();
1005 twits = new TwitList();
1006 twits.load();
1008 threadtwits = new ThreadTwitList();
1009 threadtwits.load();
1011 //concmdf!"exec \"%q/zxemut.rc\" tan"(configDir);
1012 concmd("exec nntp.rc tan");
1013 conProcessQueue(); // load config
1014 conProcessArgs!true(args);
1016 vbufEffScale = VBufScale;
1017 vbufEffVSync = vbufVSync;
1019 lastWinWidth = winWidthScaled;
1020 lastWinHeight = winHeightScaled;
1022 vbwin = new SimpleWindow(lastWinWidth, lastWinHeight, "Dingo", OpenGlOptions.yes, Resizablity.allowResizing);
1023 vbwin.hideCursor();
1025 vbwin.onFocusChange = delegate (bool focused) {
1026 if (!focused) {
1027 oldMouseButtons = 0;
1029 vbfocused = focused;
1032 vbwin.windowResized = delegate (int wdt, int hgt) {
1033 // TODO: fix gui sizes
1035 if (lastWinWidth == wdt && lastWinHeight == hgt) return;
1036 glconResize(wdt, hgt);
1038 double glwFrac = cast(double)guiGroupListWidth/VBufWidth;
1039 double tlhFrac = cast(double)guiThreadListHeight/VBufHeight;
1041 vbufEffScale = VBufScale;
1042 if (wdt < VBufScale*32) wdt = VBufScale;
1043 if (hgt < VBufScale*32) hgt = VBufScale;
1044 VBufWidth = (wdt+VBufScale-1)/VBufScale;
1045 VBufHeight = (hgt+VBufScale-1)/VBufScale;
1046 zxtexbuf.length = VBufWidth*VBufHeight+4;
1048 guiGroupListWidth = cast(int)(glwFrac*VBufWidth+0.5);
1049 guiThreadListHeight = cast(int)(tlhFrac*VBufHeight+0.5);
1051 if (guiGroupListWidth < 12) guiGroupListWidth = 12;
1052 if (guiThreadListHeight < 16) guiThreadListHeight = 16;
1054 lastWinWidth = wdt;
1055 lastWinHeight = hgt;
1057 // reinitialize OpenGL texture
1059 import iv.glbinds;
1061 enum wrapOpt = GL_REPEAT;
1062 enum filterOpt = GL_NEAREST; //GL_LINEAR;
1063 enum ttype = GL_UNSIGNED_BYTE;
1065 if (zxtexid) glDeleteTextures(1, &zxtexid);
1066 zxtexid = 0;
1067 glGenTextures(1, &zxtexid);
1068 if (zxtexid == 0) assert(0, "can't create OpenGL texture");
1070 //GLint gltextbinding;
1071 //glGetIntegerv(GL_TEXTURE_BINDING_2D, &gltextbinding);
1072 //scope(exit) glBindTexture(GL_TEXTURE_2D, gltextbinding);
1074 glBindTexture(GL_TEXTURE_2D, zxtexid);
1075 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapOpt);
1076 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapOpt);
1077 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filterOpt);
1078 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filterOpt);
1079 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
1080 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
1082 GLfloat[4] bclr = 0.0;
1083 glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, bclr.ptr);
1085 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, VBufWidth, VBufHeight, 0, GLTexType, GL_UNSIGNED_BYTE, zxtexbuf.ptr);
1088 mouseMoved();
1090 glUpdateTexture();
1091 vbwin.redrawOpenGlSceneNow();
1094 vbwin.addEventListener((DoConsoleCommandsEvent evt) {
1095 bool sendAnother = false;
1096 if (!vbwinLocked) {
1097 consoleLock();
1098 scope(exit) consoleUnlock();
1099 //auto ccwasempty = conQueueEmpty();
1100 conProcessQueue();
1101 sendAnother = !conQueueEmpty();
1102 } else {
1103 consoleLock();
1104 scope(exit) consoleUnlock();
1105 sendAnother = !conQueueEmpty();
1107 if (sendAnother) postDoConCommands();
1110 vbwin.addEventListener((HideMouseEvent evt) {
1111 if (isQuitRequested) { vbwin.close(); return; }
1112 if (vbwin.closed) return;
1113 if (!repostHideMouse) vbwin.redrawOpenGlSceneNow(); // this will hide the mouse
1116 vbwin.addEventListener((ScreenUpdateEvent evt) {
1117 //conwriteln("screen ready! ", *cast(void**)&evt);
1118 if (isQuitRequested) { vbwin.close(); return; }
1119 if (vbwin.closed) return;
1120 glUpdateTexture();
1121 vbwin.redrawOpenGlSceneNow();
1124 vbwin.addEventListener((ScreenRepaintEvent evt) {
1125 //conwriteln("screen repaint! ", *cast(void**)&evt);
1126 if (isQuitRequested) { vbwin.close(); return; }
1127 if (vbwin.closed) return;
1128 vbwin.redrawOpenGlSceneNow();
1131 vbwin.addEventListener((QuitEvent evt) {
1132 //conwriteln("quit! ", *cast(void**)&evt);
1133 if (isQuitRequested) { vbwin.close(); return; }
1134 if (vbwin.closed) return;
1135 vbwin.close();
1138 vbwin.addEventListener((UpdatingGroupEvent evt) {
1139 //conwriteln("updating group '", groups[evt.gidx].mbase.groupname);
1140 groups[evt.gidx].uiFlagUpdating = true;
1141 postScreenUpdate();
1144 vbwin.addEventListener((UpdatingGroupCompleteEvent evt) {
1145 //conwriteln("updating group '", groups[evt.gidx].mbase.groupname);
1146 groups[evt.gidx].uiFlagUpdating = false;
1147 groups[evt.gidx].buildVisibleList();
1148 postScreenUpdate();
1151 vbwin.addEventListener((UpdatingCompleteEvent evt) {
1152 //conwriteln("UPDATE COMPLETE!");
1153 if (vbwinLocked) vbwinLocked = false;
1154 glUpdateTexture();
1155 vbwin.redrawOpenGlSceneNow();
1158 vbwin.addEventListener((CursorBlinkEvent evt) {
1159 //conwriteln("cblink! ", vbwin.eventQueued!CursorBlinkEvent);
1160 glUpdateTexture();
1161 vbwin.redrawOpenGlSceneNow();
1164 vbwin.redrawOpenGlScene = delegate () {
1165 bool resizeWin = false;
1166 bool rebuildTexture = false;
1169 consoleLock();
1170 scope(exit) consoleUnlock();
1172 if (!conQueueEmpty()) postDoConCommands();
1174 if (VBufScale != vbufEffScale) {
1175 // window scale changed
1176 vbufEffScale = VBufScale;
1177 resizeWin = true;
1179 if (vbufEffVSync != vbufVSync) {
1180 vbufEffVSync = vbufVSync;
1181 vbwin.vsync = vbufEffVSync;
1185 if (resizeWin) {
1186 vbwin.resize(winWidthScaled, winHeightScaled);
1187 glconResize(winWidthScaled, winHeightScaled);
1188 rebuildTexture = true;
1191 if (rebuildTexture) glUpdateTexture();
1193 glMatrixMode(GL_PROJECTION); // for ortho camera
1194 glLoadIdentity();
1195 // left, right, bottom, top, near, far
1196 //glViewport(0, 0, w*vbufEffScale, h*vbufEffScale);
1197 //glOrtho(0, w, h, 0, -1, 1); // top-to-bottom
1198 glViewport(0, 0, VBufWidth*vbufEffScale, VBufHeight*vbufEffScale);
1199 glOrtho(0, VBufWidth, VBufHeight, 0, -1, 1); // top-to-bottom
1200 glMatrixMode(GL_MODELVIEW);
1201 glLoadIdentity();
1203 glEnable(GL_TEXTURE_2D);
1204 glDisable(GL_LIGHTING);
1205 glDisable(GL_DITHER);
1206 //glDisable(GL_BLEND);
1207 glDisable(GL_DEPTH_TEST);
1208 //glEnable(GL_BLEND);
1209 //glBlendFunc(GL_SRC_ALPHA, GL_ONE);
1210 //glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
1211 glDisable(GL_BLEND);
1212 //glDisable(GL_STENCIL_TEST);
1214 if (zxtexid) {
1215 immutable w = VBufWidth;
1216 immutable h = VBufHeight;
1218 glColor4f(1, 1, 1, 1);
1219 glBindTexture(GL_TEXTURE_2D, zxtexid);
1220 //scope(exit) glBindTexture(GL_TEXTURE_2D, 0);
1221 glBegin(GL_QUADS);
1222 glTexCoord2f(0.0f, 0.0f); glVertex2i(0, 0); // top-left
1223 glTexCoord2f(1.0f, 0.0f); glVertex2i(w, 0); // top-right
1224 glTexCoord2f(1.0f, 1.0f); glVertex2i(w, h); // bottom-right
1225 glTexCoord2f(0.0f, 1.0f); glVertex2i(0, h); // bottom-left
1226 glEnd();
1229 if (vArrowTextureId) {
1230 if (isMouseVisible) {
1231 int px = oldMouseX/vbufEffScale;
1232 int py = oldMouseY/vbufEffScale;
1233 glEnable(GL_BLEND);
1234 //glBlendFunc(GL_SRC_ALPHA, GL_ONE);
1235 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
1236 glColor4f(1, 1, 1, 1);
1237 glBindTexture(GL_TEXTURE_2D, vArrowTextureId);
1238 //scope(exit) glBindTexture(GL_TEXTURE_2D, 0);
1239 glBegin(GL_QUADS);
1240 glTexCoord2f(0.0f, 0.0f); glVertex2i(px, py); // top-left
1241 glTexCoord2f(1.0f, 0.0f); glVertex2i(px+16, py); // top-right
1242 glTexCoord2f(1.0f, 1.0f); glVertex2i(px+16, py+8); // bottom-right
1243 glTexCoord2f(0.0f, 1.0f); glVertex2i(px, py+8); // bottom-left
1244 glEnd();
1248 glconDraw();
1250 if (isQuitRequested()) vbwin.postEvent(new QuitEvent());
1253 static if (is(typeof(&vbwin.closeQuery))) {
1254 vbwin.closeQuery = delegate () { concmd("quit"); };
1257 vbwin.visibleForTheFirstTime = delegate () {
1258 import iv.glbinds;
1259 vbwin.setAsCurrentOpenGlContext();
1260 vbufEffVSync = vbufVSync;
1261 vbwin.vsync = vbufEffVSync;
1263 // initialize OpenGL texture
1265 enum wrapOpt = GL_REPEAT;
1266 enum filterOpt = GL_NEAREST; //GL_LINEAR;
1267 enum ttype = GL_UNSIGNED_BYTE;
1269 glGenTextures(1, &zxtexid);
1270 if (zxtexid == 0) assert(0, "can't create OpenGL texture");
1272 //GLint gltextbinding;
1273 //glGetIntegerv(GL_TEXTURE_BINDING_2D, &gltextbinding);
1274 //scope(exit) glBindTexture(GL_TEXTURE_2D, gltextbinding);
1276 glBindTexture(GL_TEXTURE_2D, zxtexid);
1277 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapOpt);
1278 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapOpt);
1279 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filterOpt);
1280 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filterOpt);
1281 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
1282 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
1284 GLfloat[4] bclr = 0.0;
1285 glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, bclr.ptr);
1287 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, VBufWidth, VBufHeight, 0, GLTexType, GL_UNSIGNED_BYTE, zxtexbuf.ptr);
1290 createArrowTexture();
1292 glconInit(winWidthScaled, winHeightScaled);
1293 vbwin.redrawOpenGlSceneNow();
1295 updateThreadId = spawn(&updateThread, thisTid);
1298 postScreenUpdate();
1299 repostHideMouse();
1300 foreach (Group g; groups) g.forceUpdated();
1302 vbwin.eventLoop(1000*10,
1303 delegate () {
1304 if (vbwin.closed) return;
1305 if (isQuitRequested) { vbwin.close(); return; }
1306 bool needUpdate = false;
1307 foreach (Group g; groups) if (g.needUpdate()) { needUpdate = true; break; }
1308 if (needUpdate) {
1309 //vbwinLocked = true;
1310 updateThreadId.send(UpThreadCommand.StartUpdate);
1311 //glUpdateTexture();
1312 //vbwin.redrawOpenGlSceneNow();
1314 if (!conQueueEmpty()) postDoConCommands();
1316 delegate (KeyEvent event) {
1317 if (vbwin.closed) return;
1318 if (isQuitRequested) { vbwin.close(); return; }
1319 scope(exit) {
1320 if (!conQueueEmpty()) postDoConCommands();
1322 if (glconKeyEvent(event)) {
1323 postScreenRepaint();
1324 return;
1326 if (subwins.length) {
1327 if (!vbwinLocked || subwins[$-1].immuneToLock) {
1328 if (event.pressed) ignoreSubWinChar = false;
1329 subwins[$-1].onKey(event);
1330 postScreenUpdate();
1331 } else {
1332 ignoreSubWinChar = false;
1334 return;
1336 if (event.pressed) {
1337 //conwriteln("key: ", event.toStr, ": ", event.modifierState&ModifierState.windows);
1338 if (event.pressed && event == "C-Q") { concmd("quit"); return; }
1339 if (event == "N") { concmd("next_unread ona"); return; }
1340 if (event == "S-N") { concmd("next_unread tan"); return; }
1341 if (event == "U") { concmd("mark_unread"); return; }
1342 if (event == "C-R") { concmd("mark_read"); return; }
1343 if (event == "Space") { concmd("artext_page_down"); return; }
1344 if (event == "S-Space") { concmd("artext_page_up"); return; }
1345 if (event == "Up" || event == "Pad8") { concmd("article_prev"); return; }
1346 if (event == "Down" || event == "Pad2") { concmd("article_next"); return; }
1347 if (event == "PageUp" || event == "Pad9") { concmd("article_pgup"); return; }
1348 if (event == "PageDown" || event == "Pad3") { concmd("article_pgdown"); return; }
1349 if (event == "Home" || event == "Pad7") { concmd("article_to_first"); return; }
1350 if (event == "End" || event == "Pad1") { concmd("article_to_last"); return; }
1351 if (event == "C-Up" || event == "C-Pad8") { concmd("article_scroll_up"); return; }
1352 if (event == "C-Down" || event == "C-Pad2") { concmd("article_scroll_down"); return; }
1353 if (event == "C-PageUp" || event == "C-Pad9") { concmd("group_prev"); return; }
1354 if (event == "C-PageDown" || event == "C-Pad3") { concmd("group_next"); return; }
1355 if (event == "C-M-U") { concmd("group_update"); return; }
1356 if (event == "C-H") { concmd("article_dump_headers"); return; }
1357 if (event == "C-S-I") { concmd("update_all"); return; }
1358 if (event == "C-Period") { concmd("find_mine"); return; }
1359 if (event == "C-Insert") {
1360 if (auto g = getActiveGroup) {
1361 g.withBase((abase) {
1362 if (g.curidx < g.length) {
1363 if (auto art = abase[g.baseidx(g.curidx)]) {
1364 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
1365 string id = art.msgid;
1366 if (id.length > 2 && id[0] == '<' && id[$-1] == '>') id = id[1..$-1];
1367 id = "http://forum.dlang.org/post/"~id;
1368 setClipboardText(vbwin, id);
1369 setPrimarySelection(vbwin, id);
1374 return;
1376 if (event == "T") {
1377 if (auto g = getActiveGroup) {
1378 g.withBase(delegate (abase) {
1379 if (g.curidx < g.length) {
1380 if (auto art = abase[g.baseidx(g.curidx)]) {
1381 auto t = twits.check(*art);
1382 if (t) {
1383 new TitlePrompt(art.fromname~" <"~art.frommail~">", t.name, art.msgid, t.title);
1384 } else {
1385 new TitlePrompt(art.fromname~" <"~art.frommail~">", art.fromname~" <"~art.frommail~">", art.msgid, "");
1391 return;
1393 // twit thread
1394 if (event == "C-M-K") {
1395 if (auto g = getActiveGroup) {
1396 g.withBase(delegate (abase) {
1397 if (g.curidx < g.length) {
1398 uint ridx = g.baseidx(g.curidx);
1399 if (auto art = abase[ridx]) {
1400 while (abase[ridx].parent != 0) ridx = abase[ridx].parent;
1401 threadtwits.add(art.msgid);
1402 g.moveToNextThread();
1403 void doMark (uint idx) {
1404 while (idx != 0) {
1405 auto a = abase[idx];
1406 if (a is null) break;
1407 if (a.unread) {
1408 a.unread = false;
1409 a.updated = true;
1411 doMark(a.firstchild);
1412 idx = a.nextsib;
1415 doMark(art.firstchild);
1416 g.buildVisibleList();
1420 postScreenUpdate();
1422 return;
1424 if (event == "R") {
1425 if (auto g = getActiveGroup) {
1426 g.withBase((abase) {
1427 if (g.curidx < g.length) {
1428 abase.loadContent(g.baseidx(g.curidx));
1429 if (auto art = abase[g.baseidx(g.curidx)]) {
1430 new PostEditor(g.groupname, *art);
1435 return;
1437 if (event == "S-P") {
1438 if (auto g = getActiveGroup) {
1439 g.withBase((abase) {
1440 new PostEditor(g.groupname, Article());
1443 return;
1445 if (event == "S-Enter") {
1446 if (auto g = getActiveGroup) {
1447 g.withBase((abase) {
1448 if (g.curidx < g.length) {
1449 if (auto art = abase[g.baseidx(g.curidx)]) {
1450 import std.stdio : File;
1451 import std.process;
1452 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
1453 string id = art.msgid;
1454 if (id.length > 2 && id[0] == '<' && id[$-1] == '>') id = id[1..$-1];
1455 spawnProcess(
1456 ["opera", "http://forum.dlang.org/post/"~id],
1457 File("/dev/zero"), File("/dev/null"), File("/dev/null"),
1463 return;
1466 postScreenRepaint();
1468 delegate (MouseEvent event) {
1469 if (vbwin.closed) return;
1470 scope(exit) {
1471 if (!conQueueEmpty()) postDoConCommands();
1473 oldMouseX = event.x;
1474 oldMouseY = event.y;
1475 mouseMoved();
1476 if (subwins.length) {
1477 auto win = subwins[$-1];
1478 // start drag?
1479 if (!subwinDrag && event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
1480 int mx = event.x/vbufEffScale;
1481 int my = event.y/vbufEffScale;
1482 if (mx >= win.winx && my >= win.winy && mx < win.winx+win.winw && my < win.winy+10) {
1483 subwinDrag = true;
1484 subwinDragXSpot = win.winx-mx;
1485 subwinDragYSpot = win.winy-my;
1486 postScreenUpdate();
1487 return;
1490 // draw
1491 if (subwinDrag) {
1492 win.winx = event.x/vbufEffScale+subwinDragXSpot;
1493 win.winy = event.y/vbufEffScale+subwinDragYSpot;
1495 // stop drag?
1496 if (subwinDrag && event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
1497 subwinDrag = false;
1498 postScreenUpdate();
1499 return;
1501 if (!vbwinLocked || win.immuneToLock) win.onMouse(event);
1502 postScreenUpdate();
1503 return;
1505 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
1506 int mx = event.x/vbufEffScale;
1507 int my = event.y/vbufEffScale;
1508 // select group
1509 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < groups.length*(gxTextHeightUtf+2)) {
1510 if (auto g = getActiveGroup) g.active = false;
1511 groups[my/(gxTextHeightUtf+2)].active = true;
1512 postScreenUpdate();
1513 return;
1515 // select post
1516 if (mx > guiGroupListWidth && mx < VBufWidth && my >= 0 && my < guiThreadListHeight) {
1517 if (auto g = getActiveGroup) {
1518 my -= g.ytopofs;
1519 my /= gxTextHeightUtf;
1520 g.curidx = g.msgtop+my;
1521 postScreenUpdate();
1522 return;
1526 // wheel
1527 if (event.type == MouseEventType.buttonPressed && (event.button == MouseButton.wheelUp || event.button == MouseButton.wheelDown)) {
1528 int mx = event.x/vbufEffScale;
1529 int my = event.y/vbufEffScale;
1530 // group
1531 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < VBufHeight) {
1532 if (groups.length > 0) {
1533 int gi = getActiveGroupIndex();
1534 gi += (event.button == MouseButton.wheelUp ? -1 : 1);
1535 if (gi < 0) gi = 0;
1536 if (gi >= groups.length) gi = cast(int)groups.length-1;
1537 if (auto g = getActiveGroup) g.active = false;
1538 groups[gi].active = true;
1539 postScreenUpdate();
1541 return;
1543 // select post
1544 if (mx > guiGroupListWidth && mx < VBufWidth && my >= 0 && my < guiThreadListHeight) {
1545 if (auto g = getActiveGroup) {
1546 if (event.button == MouseButton.wheelUp) g.moveUp(); else g.moveDown();
1547 postScreenUpdate();
1548 return;
1552 postScreenRepaint();
1554 delegate (dchar ch) {
1555 if (vbwin.closed) return;
1556 scope(exit) {
1557 if (!conQueueEmpty()) postDoConCommands();
1559 if (glconCharEvent(ch)) {
1560 postScreenRepaint();
1561 return;
1563 if (subwins.length) {
1564 if (!vbwinLocked || subwins[$-1].immuneToLock) {
1565 if (!ignoreSubWinChar) subwins[$-1].onChar(ch);
1566 ignoreSubWinChar = false;
1567 postScreenUpdate();
1568 } else {
1569 ignoreSubWinChar = false;
1571 return;
1575 updateThreadId.send(UpThreadCommand.Quit);