don't center cursor on message tree rebuild
[knntp.git] / nntpreader.d
blobb11934e65c77b1e7c92386b7745e163236a4612b
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.alist.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.fromDC);
313 conwrite(" ");
314 Utf8DecoderFast dc;
315 foreach (char ch; art.fromDC) {
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 foreach (immutable idx; 0..g.length) {
332 auto art = abase[g.baseidx(idx)];
333 if (art !is null) {
334 if (art.from.indexOf("ketmar") >= 0) {
335 g.curidx = cast(int)idx;
336 postScreenUpdate();
337 return;
343 })("find_mine", "find mine article");
347 // ////////////////////////////////////////////////////////////////////////// //
348 void glUpdateTexture () {
349 import iv.glbinds;
351 zxtexbuf[] = 0;
352 clipReset();
354 gxFillRect(0, 0, guiGroupListWidth, VBufHeight, gxRGB!(20, 20, 20));
355 gxVLine(guiGroupListWidth, 0, VBufHeight, gxRGB!(255, 255, 255));
357 gxFillRect(guiGroupListWidth+1, 0, VBufWidth, guiThreadListHeight, gxRGB!(15, 15, 15));
358 gxHLine(guiGroupListWidth+1, guiThreadListHeight, VBufWidth, gxRGB!(255, 255, 255));
360 void drawArticle (Group g, in ref Article art, string title) {
361 import std.format : format;
362 import std.datetime;
364 if (!art.valid || !art.contentLoaded) {
365 lastArticleText.clear();
366 articleTextTopLine = 0;
369 if (!lastArticleText.equal(g, art)) {
370 lastArticleText.set(g, art);
371 articleTextTopLine = 0;
374 clipX0 = guiGroupListWidth+2;
375 clipX1 = VBufWidth-1;
376 clipY0 = guiThreadListHeight+1;
377 clipY1 = VBufHeight-1;
379 gxFillRect(clipX0, clipY0, clipX1-clipX0+1, 3*gxTextHeightUtf+2, gxRGB!(30, 30, 30));
380 gxDrawTextUtf(clipX0+1, clipY0+0*gxTextHeightUtf+1, "From: %s".format(art.fromDC), gxRGB!(0, 128, 128));
381 gxDrawTextUtf(clipX0+1, clipY0+1*gxTextHeightUtf+1, "Subject: %s".format(art.subjDC), gxRGB!(0, 128, 128));
382 auto t = SysTime.fromUnixTime(art.time);
383 string s = "Date: %04d/%02d/%02d %02d:%02d:%02d".format(t.year, t.month, t.day, t.hour, t.minute, t.second);
384 gxDrawTextUtf(clipX0+1, clipY0+2*gxTextHeightUtf+1, s, gxRGB!(0, 128, 128));
386 //TODO: text reflow
387 int y = clipY0+3*gxTextHeightUtf+2;
388 immutable sty = y;
390 int lines = (clipY1-y)/gxTextHeightUtf;
391 if (lines < 1 || art.text.length <= lines) {
392 articleTextTopLine = 0;
393 } else {
394 if (articleTextTopLine+lines > art.text.length) {
395 articleTextTopLine = cast(int)art.text.length-lines;
396 if (articleTextTopLine < 0) articleTextTopLine = 0;
400 uint idx = articleTextTopLine;
401 while (idx < art.text.length && y < VBufHeight) {
402 int qlevel = 0;
403 s = art.text[idx];
404 uint clr = gxRGB!(0, 128, 0);
406 foreach (char ch; s) {
407 if (ch <= ' ') continue;
408 if (ch != '>') break;
409 ++qlevel;
412 clr = gxRGB!( 0, 128+40, 0);
413 if (qlevel) {
414 final switch (qlevel%2) {
415 case 0: clr = gxRGB!(128, 128, 0); break;
416 case 1: clr = gxRGB!( 0, 128, 128); break;
420 /*if (qlevel == 0 && s.length && s[0] <= ' ') {
421 gxDrawText(clipX0+1, y, s, clr);
422 } else*/ {
423 gxDrawTextUtf(clipX0+1, y, s, clr);
426 if (clipY1-y < gxTextHeightUtf && art.text.length-idx > 0) {
427 // draw "down" indicator
428 gxDrawTextOutP(clipX1-gxTextWidthP("\x1f")-3, clipY1-7, "\x1f", gxRGB!(255, 255, 255), gxRGB!(0, 69, 69));
431 ++idx;
432 y += gxTextHeightUtf;
435 if (articleTextTopLine > 0) {
436 gxDrawTextOutP(clipX1-gxTextWidthP("\x1e")-3, sty, "\x1e", gxRGB!(255, 255, 255), gxRGB!(0, 69, 69));
439 if (title.length) {
440 foreach (immutable dy; clipY0+3*gxTextHeightUtf+2..clipY1+1) {
441 foreach (immutable dx; clipX0..clipX1+1) {
442 if ((dx^dy)&1) gxPutPixel(dx, dy, gxRGB!(0, 0, 80));
446 int tx = clipX0+(clipWidth-gxTextWidthScaledUtf(3, title))/2-1;
447 int ty = clipY0+(clipHeight-3*gxTextHeightUtf)/2-1;
448 foreach (immutable dy; -1..1) {
449 foreach (immutable dx; -1..1) {
450 gxDrawTextScaledUtf(3, tx+dx, ty+dy, title, 0);
453 gxDrawTextScaledUtf(3, tx, ty, title, gxRGB!(255, 0, 0));
457 void drawThreadList (Group g) {
458 g.withBase(delegate (abase) {
459 g.makeCurrentVisible();
461 clipX0 = guiGroupListWidth+2;
462 clipX1 = VBufWidth-1-4;
463 clipY0 = 0;
464 clipY1 = guiThreadListHeight-1;
465 immutable uint origX0 = clipX0;
466 immutable uint origX1 = clipX1;
467 immutable uint origY0 = clipY0;
468 immutable uint origY1 = clipY1;
469 int y = -g.ytopofs;
470 //conwriteln(g.msgtop, " : ", g.list.length);
471 uint idx = g.msgtop;
472 while (idx < g.length && y < guiThreadListHeight) {
473 import std.format : format;
474 import std.datetime;
476 if (y >= guiThreadListHeight) break;
477 if (idx >= g.length) break;
479 clipX0 = origX0;
480 clipX1 = origX1;
482 //conwriteln(idx, " : ", g.list.length);
483 if (idx == g.curidx) gxFillRect(clipX0, y, clipX1-clipX0+1, gxTextHeightUtf, gxRGB!(0, 127, 127));
484 ++clipX0;
485 --clipX1;
487 auto art = abase[g.baseidx(idx)];
489 //uint clr = (idx != g.curidx ? gxRGB!(255, 127, 0) : gxRGB!(255, 255, 255));
490 uint clr = (art.unread ? gxRGB!(255, 255, 255) : gxRGB!(255-60, 127-60, 0));
492 if (g.twited(idx).length) clr = gxRGB!(60, 0, 0);
493 if (art.from.indexOf("ketmar@ketmar.no-ip.org") >= 0) clr = gxRGB!(0, 190, 0);
495 auto t = SysTime.fromUnixTime(art.time);
496 //string s = "%04d/%02d/%02d %02d:%02d".format(t.year, t.month, t.day, t.hour, t.minute);
497 string s = "%02d/%02d %02d:%02d".format(t.month, t.day, t.hour, t.minute);
498 gxDrawTextUtf(clipX1-gxTextWidthUtf(s), y, s, clr);
500 clipX1 -= 12*6+2;
501 gxDrawTextUtf(clipX1-20*6, y, art.fromDC, clr);
503 clipX1 -= 21*6;
504 gxDrawTextUtf(clipX0+art.depth*3, y, art.subjDC, clr);
506 ++idx;
507 y += gxTextHeightUtf;
510 // draw progressbar
512 //if (idx > g.list.length) idx = cast(uint)g.list.length;
513 clipX0 = origX0;
514 clipX1 = origX1+4;
515 clipY0 = origY0;
516 clipY1 = origY1;
517 int hgt = clipY1-clipY0+1-4;
518 int pix = cast(int)(cast(long)hgt*idx/g.length);
519 if (pix > hgt) pix = hgt;
520 gxVLine(clipX1-2, clipY0+2, pix, gxRGB!(160, 160, 160));
521 // frame
522 gxVLine(clipX1-3, clipY0+2, hgt, gxRGB!(220, 220, 220));
523 gxVLine(clipX1-1, clipY0+2, hgt, gxRGB!(220, 220, 220));
524 gxHLine(clipX1-2, clipY0+1, 1, gxRGB!(220, 220, 220));
525 gxHLine(clipX1-2, clipY1-1, 1, gxRGB!(220, 220, 220));
528 if (g.curidx < g.length) {
529 abase.loadContent(g.baseidx(g.curidx));
530 drawArticle(g, *abase[g.baseidx(g.curidx)], g.twited(g.curidx));
535 foreach (immutable idx, Group g; groups) {
536 int ofsx = 2;
537 int ofsy = 1+cast(int)idx*10;
538 clipReset();
539 if (g.active) gxFillRect(0, ofsy-1, guiGroupListWidth, 10, gxRGB!(0, 127, 127));
540 clipX0 = ofsx-1;
541 clipY0 = ofsy;
542 clipX1 = guiGroupListWidth-3;
543 uint clr = (g.unreadCount ? gxRGB!(255, 255, 0) : gxRGB!(255, 127, 0));
544 if (g.uiFlagUpdating) clr = gxRGB!(0, 255, 255);
545 gxDrawTextOutP(ofsx, ofsy, g.groupname, clr, gxRGB!(0, 0, 0));
546 if (g.active) {
547 drawThreadList(g);
551 foreach (immutable idx, SubWindow w; subwins) {
552 if (idx == subwins.length-1 && w.immuneToLock) break;
553 w.onPaint();
556 if (vbwinLocked) {
557 clipReset();
558 foreach (immutable y; 0..VBufHeight) {
559 foreach (immutable x; 0..VBufWidth) {
560 if ((x^y)&1) gxPutPixel(x, y, gxRGB!(0, 0, 99));
565 if (subwins.length && subwins[$-1].immuneToLock) subwins[$-1].onPaint();
567 if (zxtexid) {
568 glBindTexture(GL_TEXTURE_2D, zxtexid);
569 glTexSubImage2D(GL_TEXTURE_2D, 0, 0/*x*/, 0/*y*/, VBufWidth, VBufHeight, GLTexType, GL_UNSIGNED_BYTE, zxtexbuf.ptr);
570 //glBindTexture(GL_TEXTURE_2D, 0);
575 // ////////////////////////////////////////////////////////////////////////// //
576 class TitlePrompt : SubWindow {
577 string fullname;
578 string name;
579 string msgid;
580 LineEdit etitle;
582 @property string title () const pure nothrow @safe @nogc { return etitle.str; }
584 static __gshared int titleCount = 0;
586 this (string afullname, string aname, string amsgid, string atitle=null) {
587 super(260, 28);
588 if (titleCount != 0) return;
589 fullname = afullname;
590 name = aname;
591 msgid = amsgid;
592 etitle = new LineEdit();
593 etitle.str = atitle;
594 etitle.active = true;
595 immuneToLock = true;
596 add();
599 override void close () {
600 if (titleCount > 0) --titleCount;
601 super.close();
604 override void onPaint () {
605 setupClip();
606 gxDrawWindow(name, gxRGB!(255, 255, 255), gxRGB!(0, 0, 0), gxRGB!(0, 0, 180));
608 setupClientClip();
609 clipX0 += 4;
610 clipX1 -= 4;
611 clipY0 += 2;
612 etitle.onPaint();
615 override void onKey (KeyEvent event) {
616 if (!event.pressed) return;
617 if (event == "Escape") { close(); return; }
618 if (event == "Enter") {
619 twits.update(name, msgid, title, fullname);
620 if (!vbwinLocked) {
621 if (auto g = getActiveGroup) {
622 g.withBase(delegate (abase) {
623 if (g.curidx < g.length && abase[g.baseidx(g.curidx)].depth == 0) {
624 g.moveToNextThread();
626 g.buildVisibleList();
630 close();
631 return;
633 etitle.onKey(event);
636 override void onMouse (MouseEvent event) {
639 override void onChar (dchar ch) {
640 etitle.onChar(ch);
645 // ////////////////////////////////////////////////////////////////////////// //
646 class PostEditor : SubWindow {
647 static __gshared int editorCount = 0;
649 string groupname;
650 string replyid; // replyto
651 Editor editor;
652 int topline;
653 LineEdit esubj;
654 bool subjActive;
656 @property string subj () const pure nothrow @safe @nogc { return esubj.str; }
658 bool doSend () {
659 if (subj.length == 0) return false;
661 try {
662 import std.digest.sha;
663 import std.datetime;
665 conwriteln("sending to '", groupname, "'");
667 SHA256 hash;
668 hash.put(cast(const(ubyte)[])Clock.currTime().toString);
669 hash.put(cast(const(ubyte)[])groupname);
670 hash.put(cast(const(ubyte)[])replyid);
671 hash.put(cast(const(ubyte)[])subj);
672 foreach (immutable idx; 0..editor.lineCount) hash.put(cast(const(ubyte)[])editor[idx]);
673 auto hashres = hash.finish();
674 string hashstr = toHexString!(LetterCase.lower)(hashres);
675 hashstr = "<"~hashstr~"@knews>";
676 conwriteln(" message id: ", hashstr);
677 conwriteln(" reply to : ", replyid);
679 SocketNNTP sk;
680 try {
681 sk = new SocketNNTP("news.digitalmars.com");
682 } catch (Exception e) {
683 conwriteln("connection error: ", e.msg);
684 return false;
686 scope(exit) {
687 if (sk.active) sk.quit();
688 sk.close();
691 sk.selectGroup(groupname);
693 sk.doSend("%s", "POST");
694 sk.doSend("%s", "From: ketmar <ketmar@ketmar.no-ip.org>");
695 sk.doSend("Newsgroups: %s", groupname);
696 sk.doSend("Subject: %s", subj);
697 sk.doSend("Message-ID: %s", hashstr);
698 sk.doSend("%s", "Mime-Version: 1.0");
699 sk.doSend("%s", "Content-Type: text/plain; charset=utf-8; format=flowed; delsp=yes");
700 sk.doSend("%s", "Content-Transfer-Encoding: 8bit");
701 sk.doSend("%s", "User-Agent: knews");
702 if (replyid.length) sk.doSend("In-Reply-To: %s", replyid);
703 sk.doSend("%s", "");
704 foreach (immutable idx; 0..editor.lineCount) {
705 string s = editor[idx];
706 if (s.length > 0 && s[0] == '.') s = "."~s;
707 sk.doSend("%s", s);
709 sk.doSend("%s", ".");
711 auto ln = sk.readLine;
712 conwriteln(ln); // 340 Ok, recommended message-ID <o7dq4o$mpm$1@digitalmars.com>
714 if (ln.length == 0 || ln[0] != '3') throw new Exception(ln.idup);
716 foreach (Group g; groups) {
717 if (g.groupname == groupname) g.forceUpdating();
720 return true;
721 } catch (Exception e) {
722 conwriteln("SENDING ERROR: ", e.msg);
725 return false;
728 this() (string agroupname, in auto ref Article art) {
729 super(506, 253);
730 if (editorCount != 0) return;
731 if (agroupname.length == 0) return;
732 editor = new Editor();
733 esubj = new LineEdit();
734 if (art.valid) {
735 esubj.str = "Re: "~art.subj;
736 replyid = art.msgid;
738 if (art.valid && art.contentLoaded) {
739 string name = TwitList.getName(art.from);
740 if (name.length == 0) name = art.from;
741 editor.addLine(name~" wrote:");
742 editor.addLine("");
743 foreach (string s; art.text) {
744 editor.addLine(">"~s);
746 editor.reformat();
748 if (editor.lineCount == 0) editor.addLine("");
749 immuneToLock = true;
750 groupname = agroupname;
751 add();
754 override void close () {
755 if (editorCount > 0) --editorCount;
756 super.close();
759 final void textClip () {
760 setupClientClip();
761 clipX0 += 1;
762 clipX1 -= 1+4;
763 clipY0 += 12+6;
764 clipY1 -= 1;
767 final void drawSubj () {
768 setupClientClip(); // the easiest way again
769 //gxFillRect(clipX0, clipY0, clipWidth, 14, gxRGB!(0, 0, 120));
770 clipY0 += 1;
771 gxFillRect(clipX0+2, clipY0, clipWidth-4, 10, (subjActive ? gxRGB!(0, 0, 0) : gxRGB!(20, 20, 20)));
772 clipX0 += 4;
773 clipX1 -= 4;
774 clipY0 += 1;
775 esubj.active = subjActive;
776 esubj.onPaint();
779 final void makeCurVisible () {
780 textClip(); // the easiest way to get the size
781 int lvis = clipHeight/gxTextHeightUtf;
782 if (lvis < 1) lvis = 1; // just in case
783 int ltop = topline;
784 int lbot = topline+lvis-1;
785 int cy = editor.cury;
786 if (cy < ltop) {
787 topline = cy;
788 } else if (cy > lbot) {
789 topline = cy-lvis+1;
790 if (topline < 0) topline = 0;
794 final void drawScrollBar () {
795 textClip(); // the easiest way again
796 clipX1 += 4;
797 // frame
798 gxVLine(clipX1-2, clipY0+1, clipHeight-2, gxRGB!(220, 220, 220));
799 gxVLine(clipX1-0, clipY0+1, clipHeight-2, gxRGB!(220, 220, 220));
800 gxHLine(clipX1-1, clipY0, 1, gxRGB!(220, 220, 220));
801 gxHLine(clipX1-1, clipY1, 1, gxRGB!(220, 220, 220));
802 int pix = (clipHeight-2)*editor.cury/editor.lineCount;
803 if (pix > clipHeight-2) pix = clipHeight-2;
804 gxVLine(clipX1-1, clipY0+1, pix, gxRGB!(160, 160, 160));
807 override void onPaint () {
808 setupClip();
810 gxDrawWindow(groupname, gxRGB!(255, 255, 255), gxRGB!(0, 0, 0), gxRGB!(0, 0, 180));
812 textClip();
814 makeCurVisible();
816 int lidx = topline;
817 int y = clipY0;
818 while (lidx < editor.lineCount && y <= clipY1) {
819 string s = editor[lidx];
820 int qlevel = editor.quoteLevel(lidx);
821 uint clr = gxRGB!(220, 220, 0);
822 if (qlevel) {
823 final switch (qlevel%2) {
824 case 0: clr = gxRGB!(128, 128, 0); break;
825 case 1: clr = gxRGB!( 0, 128, 128); break;
828 gxDrawTextUtf(clipX0, y, s, clr);
829 if (!subjActive && lidx == editor.cury) {
830 int xpos = gxTextWidthUtf(s.utfleft(editor.curx));
831 gxVLine(clipX0+xpos, y, gxTextHeightUtf, gxRGB!(255, 255, 255));
833 ++lidx;
834 y += gxTextHeightUtf;
836 if (!subjActive && editor.cury >= editor.lineCount) gxVLine(clipX0, y, gxTextHeightUtf, gxRGB!(255, 255, 255));
837 drawScrollBar();
838 drawSubj();
841 override void onKey (KeyEvent event) {
842 if (!event.pressed) return;
843 if (event == "Escape") { close(); return; }
844 if (event == "C-Enter") {
845 if (!vbwinLocked) {
846 if (doSend()) close();
848 return;
850 if (event == "Tab") { subjActive = !subjActive; return; }
851 if (!subjActive) {
852 if (event == "Enter" || event == "PadEnter") { editor.putChar(editor.SpecCh.Enter); return; }
853 if (event == "Left" || event == "Pad4") { editor.putChar(editor.SpecCh.Left); return; }
854 if (event == "Right" || event == "Pad6") { editor.putChar(editor.SpecCh.Right); return; }
855 if (event == "Up" || event == "Pad8") { editor.putChar(editor.SpecCh.Up); return; }
856 if (event == "Down" || event == "Pad2") { editor.putChar(editor.SpecCh.Down); return; }
857 if (event == "Delete" || event == "PadDot") { editor.putChar(editor.SpecCh.Delete); return; }
858 if (event == "Home" || event == "Pad7") { editor.putChar(editor.SpecCh.Home); return; }
859 if (event == "End" || event == "Pad1") { editor.putChar(editor.SpecCh.End); return; }
860 if (event == "C-Y") { editor.putChar(editor.SpecCh.KillLine); return; }
861 if (event == "S-Insert") {
862 getClipboardText(vbwin, delegate (in char[] text) {
863 if (!closed) editor.putUtf(text[]);
866 } else {
867 esubj.onKey(event);
871 override void onMouse (MouseEvent event) {
874 override void onChar (dchar ch) {
875 if (!subjActive) {
876 if (ch == 8) { editor.putChar(ch); return; }
877 if (ch < ' ' || ch == 127) return;
878 editor.putChar(ch);
879 } else {
880 esubj.onChar(ch);
886 // ////////////////////////////////////////////////////////////////////////// //
887 void main (string[] args) {
888 sdpyWindowClass = "NNTPReader";
890 initConsole();
892 twits = new TwitList();
893 twits.load();
895 threadtwits = new ThreadTwitList();
896 threadtwits.load();
898 //concmdf!"exec \"%q/zxemut.rc\" tan"(configDir);
899 concmd("exec nntp.rc tan");
900 conProcessQueue(); // load config
901 conProcessArgs!true(args);
903 vbufEffScale = VBufScale;
904 vbufEffVSync = vbufVSync;
906 lastWinWidth = winWidthScaled;
907 lastWinHeight = winHeightScaled;
909 vbwin = new SimpleWindow(lastWinWidth, lastWinHeight, "NNTP Reader", OpenGlOptions.yes, Resizablity.allowResizing);
910 vbwin.hideCursor();
912 vbwin.onFocusChange = delegate (bool focused) {
913 if (!focused) {
914 oldMouseButtons = 0;
916 vbfocused = focused;
919 vbwin.windowResized = delegate (int wdt, int hgt) {
920 // TODO: fix gui sizes
922 if (lastWinWidth == wdt && lastWinHeight == hgt) return;
923 glconResize(wdt, hgt);
925 double glwFrac = cast(double)guiGroupListWidth/VBufWidth;
926 double tlhFrac = cast(double)guiThreadListHeight/VBufHeight;
928 vbufEffScale = VBufScale;
929 if (wdt < VBufScale*32) wdt = VBufScale;
930 if (hgt < VBufScale*32) hgt = VBufScale;
931 VBufWidth = (wdt+VBufScale-1)/VBufScale;
932 VBufHeight = (hgt+VBufScale-1)/VBufScale;
933 zxtexbuf.length = VBufWidth*VBufHeight+4;
935 guiGroupListWidth = cast(int)(glwFrac*VBufWidth+0.5);
936 guiThreadListHeight = cast(int)(tlhFrac*VBufHeight+0.5);
938 if (guiGroupListWidth < 12) guiGroupListWidth = 12;
939 if (guiThreadListHeight < 16) guiThreadListHeight = 16;
941 lastWinWidth = wdt;
942 lastWinHeight = hgt;
944 // reinitialize OpenGL texture
946 import iv.glbinds;
948 enum wrapOpt = GL_REPEAT;
949 enum filterOpt = GL_NEAREST; //GL_LINEAR;
950 enum ttype = GL_UNSIGNED_BYTE;
952 if (zxtexid) glDeleteTextures(1, &zxtexid);
953 zxtexid = 0;
954 glGenTextures(1, &zxtexid);
955 if (zxtexid == 0) assert(0, "can't create OpenGL texture");
957 //GLint gltextbinding;
958 //glGetIntegerv(GL_TEXTURE_BINDING_2D, &gltextbinding);
959 //scope(exit) glBindTexture(GL_TEXTURE_2D, gltextbinding);
961 glBindTexture(GL_TEXTURE_2D, zxtexid);
962 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapOpt);
963 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapOpt);
964 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filterOpt);
965 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filterOpt);
966 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
967 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
969 GLfloat[4] bclr = 0.0;
970 glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, bclr.ptr);
972 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, VBufWidth, VBufHeight, 0, GLTexType, GL_UNSIGNED_BYTE, zxtexbuf.ptr);
975 mouseMoved();
977 glUpdateTexture();
978 vbwin.redrawOpenGlSceneNow();
981 vbwin.addEventListener((DoConsoleCommandsEvent evt) {
982 bool sendAnother = false;
983 if (!vbwinLocked) {
984 consoleLock();
985 scope(exit) consoleUnlock();
986 //auto ccwasempty = conQueueEmpty();
987 conProcessQueue();
988 sendAnother = !conQueueEmpty();
989 } else {
990 consoleLock();
991 scope(exit) consoleUnlock();
992 sendAnother = !conQueueEmpty();
994 if (sendAnother) postDoConCommands();
997 vbwin.addEventListener((HideMouseEvent evt) {
998 if (isQuitRequested) { vbwin.close(); return; }
999 if (vbwin.closed) return;
1000 if (!repostHideMouse) vbwin.redrawOpenGlSceneNow(); // this will hide the mouse
1003 vbwin.addEventListener((ScreenUpdateEvent evt) {
1004 //conwriteln("screen ready! ", *cast(void**)&evt);
1005 if (isQuitRequested) { vbwin.close(); return; }
1006 if (vbwin.closed) return;
1007 glUpdateTexture();
1008 vbwin.redrawOpenGlSceneNow();
1011 vbwin.addEventListener((ScreenRepaintEvent evt) {
1012 //conwriteln("screen repaint! ", *cast(void**)&evt);
1013 if (isQuitRequested) { vbwin.close(); return; }
1014 if (vbwin.closed) return;
1015 vbwin.redrawOpenGlSceneNow();
1018 vbwin.addEventListener((QuitEvent evt) {
1019 //conwriteln("quit! ", *cast(void**)&evt);
1020 if (isQuitRequested) { vbwin.close(); return; }
1021 if (vbwin.closed) return;
1022 vbwin.close();
1025 vbwin.addEventListener((UpdatingGroupEvent evt) {
1026 //conwriteln("updating group '", groups[evt.gidx].mbase.groupname);
1027 groups[evt.gidx].uiFlagUpdating = true;
1028 postScreenUpdate();
1031 vbwin.addEventListener((UpdatingGroupCompleteEvent evt) {
1032 //conwriteln("updating group '", groups[evt.gidx].mbase.groupname);
1033 groups[evt.gidx].uiFlagUpdating = false;
1034 groups[evt.gidx].buildVisibleList();
1035 postScreenUpdate();
1038 vbwin.addEventListener((UpdatingCompleteEvent evt) {
1039 //conwriteln("UPDATE COMPLETE!");
1040 if (vbwinLocked) {
1041 vbwinLocked = false;
1043 glUpdateTexture();
1044 vbwin.redrawOpenGlSceneNow();
1047 vbwin.redrawOpenGlScene = delegate () {
1048 bool resizeWin = false;
1049 bool rebuildTexture = false;
1052 consoleLock();
1053 scope(exit) consoleUnlock();
1055 if (!conQueueEmpty()) postDoConCommands();
1057 if (VBufScale != vbufEffScale) {
1058 // window scale changed
1059 vbufEffScale = VBufScale;
1060 resizeWin = true;
1062 if (vbufEffVSync != vbufVSync) {
1063 vbufEffVSync = vbufVSync;
1064 vbwin.vsync = vbufEffVSync;
1068 if (resizeWin) {
1069 vbwin.resize(winWidthScaled, winHeightScaled);
1070 glconResize(winWidthScaled, winHeightScaled);
1071 rebuildTexture = true;
1074 if (rebuildTexture) glUpdateTexture();
1076 glMatrixMode(GL_PROJECTION); // for ortho camera
1077 glLoadIdentity();
1078 // left, right, bottom, top, near, far
1079 //glViewport(0, 0, w*vbufEffScale, h*vbufEffScale);
1080 //glOrtho(0, w, h, 0, -1, 1); // top-to-bottom
1081 glViewport(0, 0, VBufWidth*vbufEffScale, VBufHeight*vbufEffScale);
1082 glOrtho(0, VBufWidth, VBufHeight, 0, -1, 1); // top-to-bottom
1083 glMatrixMode(GL_MODELVIEW);
1084 glLoadIdentity();
1086 glEnable(GL_TEXTURE_2D);
1087 glDisable(GL_LIGHTING);
1088 glDisable(GL_DITHER);
1089 //glDisable(GL_BLEND);
1090 glDisable(GL_DEPTH_TEST);
1091 //glEnable(GL_BLEND);
1092 //glBlendFunc(GL_SRC_ALPHA, GL_ONE);
1093 //glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
1094 glDisable(GL_BLEND);
1095 //glDisable(GL_STENCIL_TEST);
1097 if (zxtexid) {
1098 immutable w = VBufWidth;
1099 immutable h = VBufHeight;
1101 glColor4f(1, 1, 1, 1);
1102 glBindTexture(GL_TEXTURE_2D, zxtexid);
1103 //scope(exit) glBindTexture(GL_TEXTURE_2D, 0);
1104 glBegin(GL_QUADS);
1105 glTexCoord2f(0.0f, 0.0f); glVertex2i(0, 0); // top-left
1106 glTexCoord2f(1.0f, 0.0f); glVertex2i(w, 0); // top-right
1107 glTexCoord2f(1.0f, 1.0f); glVertex2i(w, h); // bottom-right
1108 glTexCoord2f(0.0f, 1.0f); glVertex2i(0, h); // bottom-left
1109 glEnd();
1112 if (vArrowTextureId) {
1113 if (isMouseVisible) {
1114 int px = oldMouseX/vbufEffScale;
1115 int py = oldMouseY/vbufEffScale;
1116 glEnable(GL_BLEND);
1117 //glBlendFunc(GL_SRC_ALPHA, GL_ONE);
1118 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
1119 glColor4f(1, 1, 1, 1);
1120 glBindTexture(GL_TEXTURE_2D, vArrowTextureId);
1121 //scope(exit) glBindTexture(GL_TEXTURE_2D, 0);
1122 glBegin(GL_QUADS);
1123 glTexCoord2f(0.0f, 0.0f); glVertex2i(px, py); // top-left
1124 glTexCoord2f(1.0f, 0.0f); glVertex2i(px+16, py); // top-right
1125 glTexCoord2f(1.0f, 1.0f); glVertex2i(px+16, py+8); // bottom-right
1126 glTexCoord2f(0.0f, 1.0f); glVertex2i(px, py+8); // bottom-left
1127 glEnd();
1131 glconDraw();
1133 if (isQuitRequested()) vbwin.postEvent(new QuitEvent());
1136 static if (is(typeof(&vbwin.closeQuery))) {
1137 vbwin.closeQuery = delegate () { concmd("quit"); };
1140 vbwin.visibleForTheFirstTime = delegate () {
1141 import iv.glbinds;
1142 vbwin.setAsCurrentOpenGlContext();
1143 vbufEffVSync = vbufVSync;
1144 vbwin.vsync = vbufEffVSync;
1146 // initialize OpenGL texture
1148 enum wrapOpt = GL_REPEAT;
1149 enum filterOpt = GL_NEAREST; //GL_LINEAR;
1150 enum ttype = GL_UNSIGNED_BYTE;
1152 glGenTextures(1, &zxtexid);
1153 if (zxtexid == 0) assert(0, "can't create OpenGL texture");
1155 //GLint gltextbinding;
1156 //glGetIntegerv(GL_TEXTURE_BINDING_2D, &gltextbinding);
1157 //scope(exit) glBindTexture(GL_TEXTURE_2D, gltextbinding);
1159 glBindTexture(GL_TEXTURE_2D, zxtexid);
1160 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapOpt);
1161 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapOpt);
1162 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filterOpt);
1163 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filterOpt);
1164 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
1165 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
1167 GLfloat[4] bclr = 0.0;
1168 glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, bclr.ptr);
1170 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, VBufWidth, VBufHeight, 0, GLTexType, GL_UNSIGNED_BYTE, zxtexbuf.ptr);
1173 createArrowTexture();
1175 glconInit(winWidthScaled, winHeightScaled);
1176 vbwin.redrawOpenGlSceneNow();
1178 updateThreadId = spawn(&updateThread, thisTid);
1181 postScreenUpdate();
1182 repostHideMouse();
1184 vbwin.eventLoop(1000*10,
1185 delegate () {
1186 if (vbwin.closed) return;
1187 if (isQuitRequested) { vbwin.close(); return; }
1188 bool needUpdate = false;
1189 foreach (Group g; groups) if (g.needUpdate()) { needUpdate = true; break; }
1190 if (needUpdate) {
1191 //vbwinLocked = true;
1192 updateThreadId.send(UpThreadCommand.StartUpdate);
1193 //glUpdateTexture();
1194 //vbwin.redrawOpenGlSceneNow();
1196 if (!conQueueEmpty()) postDoConCommands();
1198 delegate (KeyEvent event) {
1199 if (vbwin.closed) return;
1200 if (isQuitRequested) { vbwin.close(); return; }
1201 scope(exit) {
1202 if (!conQueueEmpty()) postDoConCommands();
1204 if (glconKeyEvent(event)) {
1205 //evScrReady.send(EventId.ScreenRepaint);
1206 postScreenRepaint();
1207 return;
1209 if (event.pressed && event == "C-Q") { concmd("quit"); return; }
1210 if (subwins.length) {
1211 if (!vbwinLocked || subwins[$-1].immuneToLock) {
1212 if (event.pressed) ignoreSubWinChar = false;
1213 subwins[$-1].onKey(event);
1214 postScreenUpdate();
1215 } else {
1216 ignoreSubWinChar = false;
1218 return;
1220 if (event.pressed) {
1221 //conwriteln("key: ", event.toStr, ": ", event.modifierState&ModifierState.windows);
1222 if (event == "N") { concmd("next_unread ona"); return; }
1223 if (event == "S-N") { concmd("next_unread tan"); return; }
1224 if (event == "U") { concmd("mark_unread"); return; }
1225 if (event == "C-R") { concmd("mark_read"); return; }
1226 if (event == "Space") { concmd("artext_page_down"); return; }
1227 if (event == "S-Space") { concmd("artext_page_up"); return; }
1228 if (event == "Up" || event == "Pad8") { concmd("article_prev"); return; }
1229 if (event == "Down" || event == "Pad2") { concmd("article_next"); return; }
1230 if (event == "PageUp" || event == "Pad9") { concmd("article_pgup"); return; }
1231 if (event == "PageDown" || event == "Pad3") { concmd("article_pgdown"); return; }
1232 if (event == "Home" || event == "Pad7") { concmd("article_to_first"); return; }
1233 if (event == "End" || event == "Pad1") { concmd("article_to_last"); return; }
1234 if (event == "C-Up" || event == "C-Pad8") { concmd("article_scroll_up"); return; }
1235 if (event == "C-Down" || event == "C-Pad2") { concmd("article_scroll_down"); return; }
1236 if (event == "C-PageUp" || event == "C-Pad9") { concmd("group_prev"); return; }
1237 if (event == "C-PageDown" || event == "C-Pad3") { concmd("group_next"); return; }
1238 if (event == "C-M-U") { concmd("group_update"); return; }
1239 if (event == "C-H") { concmd("article_dump_headers"); return; }
1240 if (event == "C-S-I") { concmd("update_all"); return; }
1241 if (event == "C-Insert") { concmd("find_mine"); return; }
1242 if (event == "T") {
1243 if (auto g = getActiveGroup) {
1244 g.withBase(delegate (abase) {
1245 if (g.curidx < g.length) {
1246 if (auto art = abase[g.baseidx(g.curidx)]) {
1247 auto t = twits.check(*art);
1248 if (t) {
1249 new TitlePrompt(art.from, t.name, art.msgid, t.title);
1250 } else {
1251 new TitlePrompt(art.from, art.from, art.msgid, "");
1257 return;
1259 // twit thread
1260 if (event == "C-M-K") {
1261 if (auto g = getActiveGroup) {
1262 g.withBase(delegate (abase) {
1263 if (g.curidx < g.length) {
1264 uint ridx = g.baseidx(g.curidx);
1265 if (auto art = abase[ridx]) {
1266 if (art.rootidx != uint.max) {
1267 ridx = art.rootidx;
1268 art = abase[art.rootidx];
1269 assert(art !is null);
1271 threadtwits.add(art.msgid);
1272 g.moveToNextThread();
1273 void doMark (uint idx) {
1274 while (idx != uint.max) {
1275 auto a = abase[idx];
1276 if (a is null) break;
1277 if (a.unread) {
1278 a.unread = false;
1279 a.updated = true;
1281 doMark(a.firstchildidx);
1282 idx = a.nextsibidx;
1285 doMark(art.firstchildidx);
1286 g.buildVisibleList();
1290 postScreenUpdate();
1292 return;
1294 if (event == "R") {
1295 if (auto g = getActiveGroup) {
1296 g.withBase((abase) {
1297 if (g.curidx < g.length) {
1298 abase.loadContent(g.baseidx(g.curidx));
1299 if (auto art = abase[g.baseidx(g.curidx)]) {
1300 new PostEditor(g.groupname, *art);
1305 return;
1307 if (event == "S-P") {
1308 if (auto g = getActiveGroup) {
1309 g.withBase((abase) {
1310 new PostEditor(g.groupname, Article());
1313 return;
1315 if (event == "S-Enter") {
1316 if (auto g = getActiveGroup) {
1317 g.withBase((abase) {
1318 if (g.curidx < g.length) {
1319 if (auto art = abase[g.baseidx(g.curidx)]) {
1320 import std.stdio : File;
1321 import std.process;
1322 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
1323 string id = art.msgid;
1324 if (id.length > 2 && id[0] == '<' && id[$-1] == '>') id = id[1..$-1];
1325 spawnProcess(
1326 ["opera", "http://forum.dlang.org/post/"~id],
1327 File("/dev/zero"), File("/dev/null"), File("/dev/null"),
1333 return;
1336 postScreenRepaint();
1338 delegate (MouseEvent event) {
1339 if (vbwin.closed) return;
1340 scope(exit) {
1341 if (!conQueueEmpty()) postDoConCommands();
1343 oldMouseX = event.x;
1344 oldMouseY = event.y;
1345 mouseMoved();
1346 if (subwins.length) {
1347 if (!vbwinLocked || subwins[$-1].immuneToLock) {
1348 subwins[$-1].onMouse(event);
1349 postScreenUpdate();
1351 return;
1353 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
1354 int mx = event.x/vbufEffScale;
1355 int my = event.y/vbufEffScale;
1356 // select group
1357 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < groups.length*10) {
1358 if (auto g = getActiveGroup) g.active = false;
1359 groups[my/10].active = true;
1360 postScreenUpdate();
1361 return;
1363 // select post
1364 if (mx > guiGroupListWidth && mx < VBufWidth && my >= 0 && my < guiThreadListHeight) {
1365 if (auto g = getActiveGroup) {
1366 my -= g.ytopofs;
1367 my /= gxTextHeightUtf;
1368 g.curidx = g.msgtop+my;
1369 postScreenUpdate();
1370 return;
1374 // wheel
1375 if (event.type == MouseEventType.buttonPressed && (event.button == MouseButton.wheelUp || event.button == MouseButton.wheelDown)) {
1376 int mx = event.x/vbufEffScale;
1377 int my = event.y/vbufEffScale;
1378 // group
1379 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < VBufHeight) {
1380 if (groups.length > 0) {
1381 int gi = getActiveGroupIndex();
1382 gi += (event.button == MouseButton.wheelUp ? -1 : 1);
1383 if (gi < 0) gi = 0;
1384 if (gi >= groups.length) gi = cast(int)groups.length-1;
1385 if (auto g = getActiveGroup) g.active = false;
1386 groups[gi].active = true;
1387 postScreenUpdate();
1389 return;
1391 // select post
1392 if (mx > guiGroupListWidth && mx < VBufWidth && my >= 0 && my < guiThreadListHeight) {
1393 if (auto g = getActiveGroup) {
1394 if (event.button == MouseButton.wheelUp) g.moveUp(); else g.moveDown();
1395 postScreenUpdate();
1396 return;
1400 postScreenRepaint();
1402 delegate (dchar ch) {
1403 if (vbwin.closed) return;
1404 scope(exit) {
1405 if (!conQueueEmpty()) postDoConCommands();
1407 if (glconCharEvent(ch)) {
1408 //evScrReady.send(EventId.ScreenRepaint);
1409 postScreenRepaint();
1410 return;
1412 if (subwins.length) {
1413 if (!vbwinLocked || subwins[$-1].immuneToLock) {
1414 if (!ignoreSubWinChar) subwins[$-1].onChar(ch);
1415 ignoreSubWinChar = false;
1416 postScreenUpdate();
1417 } else {
1418 ignoreSubWinChar = false;
1420 return;
1424 updateThreadId.send(UpThreadCommand.Quit);