set primary selection
[knntp.git] / nntpreader.d
blobafd730299562489ab9a00c99d8a310a3abd80e63
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("\x1f")-3, clipY1-7, "\x1f", gxRGB!(255, 255, 255), gxRGB!(0, 69, 69));
440 ++idx;
441 y += gxTextHeightUtf;
444 if (articleTextTopLine > 0) {
445 gxDrawTextOutP(clipX1-gxTextWidthP("\x1e")-3, sty, "\x1e", 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));
501 if (g.twited(idx).length) clr = gxRGB!(60, 0, 0);
502 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) clr = gxRGB!(0, 190, 0);
504 auto t = SysTime.fromUnixTime(art.time);
505 //string s = "%04d/%02d/%02d %02d:%02d".format(t.year, t.month, t.day, t.hour, t.minute);
507 import core.stdc.stdio : snprintf;
508 char[128] tmpbuf;
509 //string s = "%02d/%02d %02d:%02d".format(t.month, t.day, t.hour, t.minute);
510 auto len = snprintf(tmpbuf.ptr, tmpbuf.length, "%02d/%02d/%02d %02d:%02d", t.year%100, t.month, t.day, t.hour, t.minute);
511 gxDrawTextUtf(clipX1-gxTextWidthUtf(tmpbuf[0..len]), y, tmpbuf[0..len], clr);
514 clipX1 -= 15*6+4;
515 gxDrawTextUtf(clipX1-20*6, y, art.fromname, clr);
516 gxDrawTextUtf(clipX1-20*6+gxTextWidthUtf(art.fromname)+4, y, "<", clr);
517 gxDrawTextUtf(clipX1-20*6+gxTextWidthUtf(art.fromname)+4+gxTextWidthUtf("<")+1, y, art.frommail, clr);
518 gxDrawTextUtf(clipX1-20*6+gxTextWidthUtf(art.fromname)+4+gxTextWidthUtf("<")+1+gxTextWidthUtf(art.frommail)+1, y, ">", clr);
520 clipX1 -= 21*6;
521 gxDrawTextUtf(clipX0+art.depth*3, y, art.subj, clr);
523 ++idx;
524 y += gxTextHeightUtf;
527 // draw progressbar
529 //if (idx > g.list.length) idx = cast(uint)g.list.length;
530 clipX0 = origX0;
531 clipX1 = origX1+4;
532 clipY0 = origY0;
533 clipY1 = origY1;
534 int hgt = clipY1-clipY0+1-4;
535 int pix = cast(int)(cast(long)hgt*idx/g.length);
536 if (pix > hgt) pix = hgt;
537 gxVLine(clipX1-2, clipY0+2, pix, gxRGB!(160, 160, 160));
538 // frame
539 gxVLine(clipX1-3, clipY0+2, hgt, gxRGB!(220, 220, 220));
540 gxVLine(clipX1-1, clipY0+2, hgt, gxRGB!(220, 220, 220));
541 gxHLine(clipX1-2, clipY0+1, 1, gxRGB!(220, 220, 220));
542 gxHLine(clipX1-2, clipY1-1, 1, gxRGB!(220, 220, 220));
545 if (g.curidx < g.length) {
546 abase.loadContent(g.baseidx(g.curidx));
547 drawArticle(g, *abase[g.baseidx(g.curidx)], g.twited(g.curidx));
552 foreach (immutable idx, Group g; groups) {
553 int ofsx = 2;
554 int ofsy = 1+cast(int)idx*10;
555 clipReset();
556 if (g.active) gxFillRect(0, ofsy-1, guiGroupListWidth, 10, gxRGB!(0, 127, 127));
557 clipX0 = ofsx-1;
558 clipY0 = ofsy;
559 clipX1 = guiGroupListWidth-3;
560 uint clr = (g.unreadCount ? gxRGB!(255, 255, 0) : gxRGB!(255, 127, 0));
561 if (g.uiFlagUpdating) clr = gxRGB!(0, 255, 255);
562 gxDrawTextOutP(ofsx, ofsy, g.groupname, clr, gxRGB!(0, 0, 0));
563 if (g.active) {
564 drawThreadList(g);
568 foreach (immutable idx, SubWindow w; subwins) {
569 if (idx == subwins.length-1 && w.immuneToLock) break;
570 w.onPaint();
573 if (vbwinLocked) {
574 clipReset();
575 foreach (immutable y; 0..VBufHeight) {
576 foreach (immutable x; 0..VBufWidth) {
577 if ((x^y)&1) gxPutPixel(x, y, gxRGB!(0, 0, 99));
582 if (subwins.length && subwins[$-1].immuneToLock) subwins[$-1].onPaint();
584 if (zxtexid) {
585 glBindTexture(GL_TEXTURE_2D, zxtexid);
586 glTexSubImage2D(GL_TEXTURE_2D, 0, 0/*x*/, 0/*y*/, VBufWidth, VBufHeight, GLTexType, GL_UNSIGNED_BYTE, zxtexbuf.ptr);
587 //glBindTexture(GL_TEXTURE_2D, 0);
592 // ////////////////////////////////////////////////////////////////////////// //
593 class TitlePrompt : SubWindow {
594 string fullname;
595 string name;
596 string msgid;
597 LineEdit etitle;
599 @property string title () const pure nothrow @safe @nogc { return etitle.str; }
601 static __gshared int titleCount = 0;
603 this (string afullname, string aname, string amsgid, string atitle=null) {
604 super(260, 28);
605 if (titleCount != 0) return;
606 fullname = afullname;
607 name = aname;
608 msgid = amsgid;
609 etitle = new LineEdit();
610 etitle.str = atitle;
611 etitle.active = true;
612 immuneToLock = true;
613 add();
616 override void close () {
617 if (titleCount > 0) --titleCount;
618 super.close();
621 override void onPaint () {
622 setupClip();
623 gxDrawWindow(name, gxRGB!(255, 255, 255), gxRGB!(0, 0, 0), gxRGB!(0, 0, 180));
625 setupClientClip();
626 clipX0 += 4;
627 clipX1 -= 4;
628 clipY0 += 2;
629 etitle.onPaint();
632 override void onKey (KeyEvent event) {
633 if (!event.pressed) return;
634 if (event == "Escape") { close(); return; }
635 if (event == "Enter") {
636 twits.update(name, msgid, title, fullname);
637 if (!vbwinLocked) {
638 if (auto g = getActiveGroup) {
639 g.withBase(delegate (abase) {
640 if (g.curidx < g.length && abase[g.baseidx(g.curidx)].depth == 0) {
641 g.moveToNextThread();
643 g.buildVisibleList();
647 close();
648 return;
650 etitle.onKey(event);
653 override void onMouse (MouseEvent event) {
656 override void onChar (dchar ch) {
657 etitle.onChar(ch);
662 // ////////////////////////////////////////////////////////////////////////// //
663 class PostEditor : SubWindow {
664 static __gshared int editorCount = 0;
666 string groupname;
667 string replyid; // replyto
668 Editor editor;
669 int topline;
670 LineEdit esubj;
671 bool subjActive;
673 @property string subj () const pure nothrow @safe @nogc { return esubj.str; }
675 bool doSend () {
676 if (subj.length == 0) return false;
678 try {
679 import std.digest.sha;
680 import std.datetime;
681 import std.uuid;
683 conwriteln("sending to '", groupname, "'");
686 SHA256 hash;
687 hash.put(cast(const(ubyte)[])Clock.currTime().toString);
688 hash.put(cast(const(ubyte)[])groupname);
689 hash.put(cast(const(ubyte)[])replyid);
690 hash.put(cast(const(ubyte)[])subj);
691 foreach (immutable idx; 0..editor.lineCount) hash.put(cast(const(ubyte)[])editor[idx]);
692 auto hashres = hash.finish();
693 string hashstr = toHexString!(LetterCase.lower)(hashres);
695 UUID id = randomUUID();
696 string hashstr = toHexString!(LetterCase.lower)(id.data);
697 hashstr = "<"~hashstr~"@knews>";
698 conwriteln(" message id: ", hashstr);
699 conwriteln(" reply to : ", replyid);
701 SocketNNTP sk;
702 try {
703 sk = new SocketNNTP("news.digitalmars.com");
704 } catch (Exception e) {
705 conwriteln("connection error: ", e.msg);
706 return false;
708 scope(exit) {
709 if (sk.active) sk.quit();
710 sk.close();
713 sk.selectGroup(groupname);
715 sk.doSend("%s", "POST");
716 sk.doSend("%s", "From: ketmar <ketmar@ketmar.no-ip.org>");
717 sk.doSend("Newsgroups: %s", groupname);
718 sk.doSend("Subject: %s", subj);
719 sk.doSend("Message-ID: %s", hashstr);
720 sk.doSend("%s", "Mime-Version: 1.0");
721 sk.doSend("%s", "Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no");
722 sk.doSend("%s", "Content-Transfer-Encoding: 8bit");
723 sk.doSend("%s", "User-Agent: knews");
724 if (replyid.length) sk.doSend("In-Reply-To: %s", replyid);
725 sk.doSend("%s", "");
726 foreach (immutable idx; 0..editor.lineCount) {
727 string s = editor[idx];
728 if (s.length > 0 && s[0] == '.') s = "."~s;
729 sk.doSend("%s", s);
731 sk.doSend("%s", ".");
733 auto ln = sk.readLine;
734 conwriteln(ln); // 340 Ok, recommended message-ID <o7dq4o$mpm$1@digitalmars.com>
736 if (ln.length == 0 || ln[0] != '3') throw new Exception(ln.idup);
738 foreach (Group g; groups) {
739 if (g.groupname == groupname) g.forceUpdating();
742 return true;
743 } catch (Exception e) {
744 conwriteln("SENDING ERROR: ", e.msg);
747 return false;
750 this() (string agroupname, in auto ref Article art) {
751 super(506, 253);
752 if (editorCount != 0) return;
753 if (agroupname.length == 0) return;
754 editor = new Editor();
755 esubj = new LineEdit();
756 if (art.valid) {
757 esubj.str = "Re: "~art.subj;
758 replyid = art.msgid;
760 if (art.valid && art.contentLoaded) {
761 editor.addLine(art.fromname~" wrote:");
762 editor.addLine("");
763 foreach (string s; art.text) {
764 editor.addLine(">"~s);
766 editor.addLine("");
767 editor.reformat();
769 if (editor.lineCount == 0) editor.addLine("");
770 immuneToLock = true;
771 groupname = agroupname;
772 add();
775 override void close () {
776 if (editorCount > 0) --editorCount;
777 super.close();
780 final void textClip () {
781 setupClientClip();
782 clipX0 += 1;
783 clipX1 -= 1+4;
784 clipY0 += 12+6;
785 clipY1 -= 1;
788 final void drawSubj () {
789 setupClientClip(); // the easiest way again
790 //gxFillRect(clipX0, clipY0, clipWidth, 14, gxRGB!(0, 0, 120));
791 clipY0 += 1;
792 gxFillRect(clipX0+2, clipY0, clipWidth-4, 10, (subjActive ? gxRGB!(0, 0, 0) : gxRGB!(20, 20, 20)));
793 clipX0 += 4;
794 clipX1 -= 4;
795 clipY0 += 1;
796 esubj.active = subjActive;
797 esubj.onPaint();
800 final void makeCurVisible () {
801 textClip(); // the easiest way to get the size
802 int lvis = clipHeight/gxTextHeightUtf;
803 if (lvis < 1) lvis = 1; // just in case
804 int ltop = topline;
805 int lbot = topline+lvis-1;
806 int cy = editor.cury;
807 if (cy < ltop) {
808 topline = cy;
809 } else if (cy > lbot) {
810 topline = cy-lvis+1;
811 if (topline < 0) topline = 0;
815 final void drawScrollBar () {
816 textClip(); // the easiest way again
817 clipX1 += 4;
818 // frame
819 gxVLine(clipX1-2, clipY0+1, clipHeight-2, gxRGB!(220, 220, 220));
820 gxVLine(clipX1-0, clipY0+1, clipHeight-2, gxRGB!(220, 220, 220));
821 gxHLine(clipX1-1, clipY0, 1, gxRGB!(220, 220, 220));
822 gxHLine(clipX1-1, clipY1, 1, gxRGB!(220, 220, 220));
823 int pix = (clipHeight-2)*editor.cury/editor.lineCount;
824 if (pix > clipHeight-2) pix = clipHeight-2;
825 gxVLine(clipX1-1, clipY0+1, pix, gxRGB!(160, 160, 160));
828 override void onPaint () {
829 setupClip();
831 gxDrawWindow(groupname, gxRGB!(255, 255, 255), gxRGB!(0, 0, 0), gxRGB!(0, 0, 180));
833 textClip();
835 makeCurVisible();
837 int lidx = topline;
838 int y = clipY0;
839 while (lidx < editor.lineCount && y <= clipY1) {
840 string s = editor[lidx];
841 int qlevel = editor.quoteLevel(lidx);
842 uint clr = gxRGB!(220, 220, 0);
843 uint markBgClr = gxRGB!(227, 0, 195);
844 if (qlevel) {
845 final switch (qlevel%2) {
846 case 0: clr = gxRGB!(128, 128, 0); break;
847 case 1: clr = gxRGB!( 0, 128, 128); break;
850 if (!editor.lineHasMark(lidx)) {
851 gxDrawTextUtf(clipX0, y, s, clr);
852 } else {
853 int xx = clipX0;
854 int cx = 0;
855 utfByLocal(editor[lidx], delegate (char ch) {
856 int w = gxCharWidthP(ch)+1;
857 if (editor.isMarked(cx, lidx)) {
858 gxFillRect(xx, y, w-(editor.isMarked(cx+1, lidx) ? 0 : 1), gxTextHeightUtf, markBgClr);
860 gxDrawCharP(xx, y, ch, clr);
861 xx += w;
862 ++cx;
865 if (!subjActive && lidx == editor.cury) {
866 int xpos = gxTextWidthUtf(s.utfleft(editor.curx));
867 drawTextCursor(clipX0+xpos, y);
869 ++lidx;
870 y += gxTextHeightUtf;
872 if (!subjActive && editor.cury >= editor.lineCount) drawTextCursor(clipX0, y);
873 drawScrollBar();
874 drawSubj();
875 if (!subjActive) postCurBlink();
878 override void onKey (KeyEvent event) {
879 if (!event.pressed) return;
880 if (event == "C-Escape" || event == "M-Escape") { close(); return; }
881 if (event == "C-Enter") {
882 if (!vbwinLocked) {
883 if (doSend()) close();
885 return;
887 if (event == "Tab") { subjActive = !subjActive; return; }
888 if (!subjActive) {
889 if (event == "Escape") { editor.putChar(editor.SpecCh.ResetMark); return; }
890 if (event == "C-Space") { editor.putChar(editor.SpecCh.PutMark); return; }
891 if (event == "Enter" || event == "PadEnter") { editor.putChar(editor.SpecCh.Enter); return; }
892 if (event == "Left" || event == "Pad4") { editor.putChar(editor.SpecCh.Left); return; }
893 if (event == "Right" || event == "Pad6") { editor.putChar(editor.SpecCh.Right); return; }
894 if (event == "Up" || event == "Pad8") { editor.putChar(editor.SpecCh.Up); return; }
895 if (event == "Down" || event == "Pad2") { editor.putChar(editor.SpecCh.Down); return; }
896 if (event == "Delete" || event == "PadDot") { editor.putChar(editor.SpecCh.Delete); return; }
897 if (event == "Home" || event == "Pad7") { editor.putChar(editor.SpecCh.Home); return; }
898 if (event == "End" || event == "Pad1") { editor.putChar(editor.SpecCh.End); return; }
899 if (event == "C-Y") { editor.putChar(editor.SpecCh.KillLine); return; }
900 if (event == "S-Insert") {
901 getClipboardText(vbwin, delegate (in char[] text) {
902 if (!closed) editor.putUtf(text[]);
904 return;
906 if (event == "C-Insert") {
907 string ct = editor.getSelectionText();
908 if (ct.length > 0) {
909 setClipboardText(vbwin, ct);
910 setPrimarySelection(vbwin, ct);
912 editor.putChar(editor.SpecCh.ResetMark);
913 return;
915 } else {
916 esubj.onKey(event);
920 override void onMouse (MouseEvent event) {
923 override void onChar (dchar ch) {
924 if (!subjActive) {
925 if (ch == 8) { editor.putChar(ch); return; }
926 if (ch < ' ' || ch == 127) return;
927 editor.putChar(ch);
928 } else {
929 esubj.onChar(ch);
935 // ////////////////////////////////////////////////////////////////////////// //
936 void main (string[] args) {
937 sdpyWindowClass = "NNTPReader";
938 glconShowKey = "C-Grave";
940 initConsole();
942 twits = new TwitList();
943 twits.load();
945 threadtwits = new ThreadTwitList();
946 threadtwits.load();
948 //concmdf!"exec \"%q/zxemut.rc\" tan"(configDir);
949 concmd("exec nntp.rc tan");
950 conProcessQueue(); // load config
951 conProcessArgs!true(args);
953 vbufEffScale = VBufScale;
954 vbufEffVSync = vbufVSync;
956 lastWinWidth = winWidthScaled;
957 lastWinHeight = winHeightScaled;
959 vbwin = new SimpleWindow(lastWinWidth, lastWinHeight, "NNTP Reader", OpenGlOptions.yes, Resizablity.allowResizing);
960 vbwin.hideCursor();
962 vbwin.onFocusChange = delegate (bool focused) {
963 if (!focused) {
964 oldMouseButtons = 0;
966 vbfocused = focused;
969 vbwin.windowResized = delegate (int wdt, int hgt) {
970 // TODO: fix gui sizes
972 if (lastWinWidth == wdt && lastWinHeight == hgt) return;
973 glconResize(wdt, hgt);
975 double glwFrac = cast(double)guiGroupListWidth/VBufWidth;
976 double tlhFrac = cast(double)guiThreadListHeight/VBufHeight;
978 vbufEffScale = VBufScale;
979 if (wdt < VBufScale*32) wdt = VBufScale;
980 if (hgt < VBufScale*32) hgt = VBufScale;
981 VBufWidth = (wdt+VBufScale-1)/VBufScale;
982 VBufHeight = (hgt+VBufScale-1)/VBufScale;
983 zxtexbuf.length = VBufWidth*VBufHeight+4;
985 guiGroupListWidth = cast(int)(glwFrac*VBufWidth+0.5);
986 guiThreadListHeight = cast(int)(tlhFrac*VBufHeight+0.5);
988 if (guiGroupListWidth < 12) guiGroupListWidth = 12;
989 if (guiThreadListHeight < 16) guiThreadListHeight = 16;
991 lastWinWidth = wdt;
992 lastWinHeight = hgt;
994 // reinitialize OpenGL texture
996 import iv.glbinds;
998 enum wrapOpt = GL_REPEAT;
999 enum filterOpt = GL_NEAREST; //GL_LINEAR;
1000 enum ttype = GL_UNSIGNED_BYTE;
1002 if (zxtexid) glDeleteTextures(1, &zxtexid);
1003 zxtexid = 0;
1004 glGenTextures(1, &zxtexid);
1005 if (zxtexid == 0) assert(0, "can't create OpenGL texture");
1007 //GLint gltextbinding;
1008 //glGetIntegerv(GL_TEXTURE_BINDING_2D, &gltextbinding);
1009 //scope(exit) glBindTexture(GL_TEXTURE_2D, gltextbinding);
1011 glBindTexture(GL_TEXTURE_2D, zxtexid);
1012 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapOpt);
1013 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapOpt);
1014 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filterOpt);
1015 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filterOpt);
1016 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
1017 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
1019 GLfloat[4] bclr = 0.0;
1020 glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, bclr.ptr);
1022 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, VBufWidth, VBufHeight, 0, GLTexType, GL_UNSIGNED_BYTE, zxtexbuf.ptr);
1025 mouseMoved();
1027 glUpdateTexture();
1028 vbwin.redrawOpenGlSceneNow();
1031 vbwin.addEventListener((DoConsoleCommandsEvent evt) {
1032 bool sendAnother = false;
1033 if (!vbwinLocked) {
1034 consoleLock();
1035 scope(exit) consoleUnlock();
1036 //auto ccwasempty = conQueueEmpty();
1037 conProcessQueue();
1038 sendAnother = !conQueueEmpty();
1039 } else {
1040 consoleLock();
1041 scope(exit) consoleUnlock();
1042 sendAnother = !conQueueEmpty();
1044 if (sendAnother) postDoConCommands();
1047 vbwin.addEventListener((HideMouseEvent evt) {
1048 if (isQuitRequested) { vbwin.close(); return; }
1049 if (vbwin.closed) return;
1050 if (!repostHideMouse) vbwin.redrawOpenGlSceneNow(); // this will hide the mouse
1053 vbwin.addEventListener((ScreenUpdateEvent evt) {
1054 //conwriteln("screen ready! ", *cast(void**)&evt);
1055 if (isQuitRequested) { vbwin.close(); return; }
1056 if (vbwin.closed) return;
1057 glUpdateTexture();
1058 vbwin.redrawOpenGlSceneNow();
1061 vbwin.addEventListener((ScreenRepaintEvent evt) {
1062 //conwriteln("screen repaint! ", *cast(void**)&evt);
1063 if (isQuitRequested) { vbwin.close(); return; }
1064 if (vbwin.closed) return;
1065 vbwin.redrawOpenGlSceneNow();
1068 vbwin.addEventListener((QuitEvent evt) {
1069 //conwriteln("quit! ", *cast(void**)&evt);
1070 if (isQuitRequested) { vbwin.close(); return; }
1071 if (vbwin.closed) return;
1072 vbwin.close();
1075 vbwin.addEventListener((UpdatingGroupEvent evt) {
1076 //conwriteln("updating group '", groups[evt.gidx].mbase.groupname);
1077 groups[evt.gidx].uiFlagUpdating = true;
1078 postScreenUpdate();
1081 vbwin.addEventListener((UpdatingGroupCompleteEvent evt) {
1082 //conwriteln("updating group '", groups[evt.gidx].mbase.groupname);
1083 groups[evt.gidx].uiFlagUpdating = false;
1084 groups[evt.gidx].buildVisibleList();
1085 postScreenUpdate();
1088 vbwin.addEventListener((UpdatingCompleteEvent evt) {
1089 //conwriteln("UPDATE COMPLETE!");
1090 if (vbwinLocked) vbwinLocked = false;
1091 glUpdateTexture();
1092 vbwin.redrawOpenGlSceneNow();
1095 vbwin.addEventListener((CursorBlinkEvent evt) {
1096 //conwriteln("cblink! ", vbwin.eventQueued!CursorBlinkEvent);
1097 glUpdateTexture();
1098 vbwin.redrawOpenGlSceneNow();
1101 vbwin.redrawOpenGlScene = delegate () {
1102 bool resizeWin = false;
1103 bool rebuildTexture = false;
1106 consoleLock();
1107 scope(exit) consoleUnlock();
1109 if (!conQueueEmpty()) postDoConCommands();
1111 if (VBufScale != vbufEffScale) {
1112 // window scale changed
1113 vbufEffScale = VBufScale;
1114 resizeWin = true;
1116 if (vbufEffVSync != vbufVSync) {
1117 vbufEffVSync = vbufVSync;
1118 vbwin.vsync = vbufEffVSync;
1122 if (resizeWin) {
1123 vbwin.resize(winWidthScaled, winHeightScaled);
1124 glconResize(winWidthScaled, winHeightScaled);
1125 rebuildTexture = true;
1128 if (rebuildTexture) glUpdateTexture();
1130 glMatrixMode(GL_PROJECTION); // for ortho camera
1131 glLoadIdentity();
1132 // left, right, bottom, top, near, far
1133 //glViewport(0, 0, w*vbufEffScale, h*vbufEffScale);
1134 //glOrtho(0, w, h, 0, -1, 1); // top-to-bottom
1135 glViewport(0, 0, VBufWidth*vbufEffScale, VBufHeight*vbufEffScale);
1136 glOrtho(0, VBufWidth, VBufHeight, 0, -1, 1); // top-to-bottom
1137 glMatrixMode(GL_MODELVIEW);
1138 glLoadIdentity();
1140 glEnable(GL_TEXTURE_2D);
1141 glDisable(GL_LIGHTING);
1142 glDisable(GL_DITHER);
1143 //glDisable(GL_BLEND);
1144 glDisable(GL_DEPTH_TEST);
1145 //glEnable(GL_BLEND);
1146 //glBlendFunc(GL_SRC_ALPHA, GL_ONE);
1147 //glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
1148 glDisable(GL_BLEND);
1149 //glDisable(GL_STENCIL_TEST);
1151 if (zxtexid) {
1152 immutable w = VBufWidth;
1153 immutable h = VBufHeight;
1155 glColor4f(1, 1, 1, 1);
1156 glBindTexture(GL_TEXTURE_2D, zxtexid);
1157 //scope(exit) glBindTexture(GL_TEXTURE_2D, 0);
1158 glBegin(GL_QUADS);
1159 glTexCoord2f(0.0f, 0.0f); glVertex2i(0, 0); // top-left
1160 glTexCoord2f(1.0f, 0.0f); glVertex2i(w, 0); // top-right
1161 glTexCoord2f(1.0f, 1.0f); glVertex2i(w, h); // bottom-right
1162 glTexCoord2f(0.0f, 1.0f); glVertex2i(0, h); // bottom-left
1163 glEnd();
1166 if (vArrowTextureId) {
1167 if (isMouseVisible) {
1168 int px = oldMouseX/vbufEffScale;
1169 int py = oldMouseY/vbufEffScale;
1170 glEnable(GL_BLEND);
1171 //glBlendFunc(GL_SRC_ALPHA, GL_ONE);
1172 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
1173 glColor4f(1, 1, 1, 1);
1174 glBindTexture(GL_TEXTURE_2D, vArrowTextureId);
1175 //scope(exit) glBindTexture(GL_TEXTURE_2D, 0);
1176 glBegin(GL_QUADS);
1177 glTexCoord2f(0.0f, 0.0f); glVertex2i(px, py); // top-left
1178 glTexCoord2f(1.0f, 0.0f); glVertex2i(px+16, py); // top-right
1179 glTexCoord2f(1.0f, 1.0f); glVertex2i(px+16, py+8); // bottom-right
1180 glTexCoord2f(0.0f, 1.0f); glVertex2i(px, py+8); // bottom-left
1181 glEnd();
1185 glconDraw();
1187 if (isQuitRequested()) vbwin.postEvent(new QuitEvent());
1190 static if (is(typeof(&vbwin.closeQuery))) {
1191 vbwin.closeQuery = delegate () { concmd("quit"); };
1194 vbwin.visibleForTheFirstTime = delegate () {
1195 import iv.glbinds;
1196 vbwin.setAsCurrentOpenGlContext();
1197 vbufEffVSync = vbufVSync;
1198 vbwin.vsync = vbufEffVSync;
1200 // initialize OpenGL texture
1202 enum wrapOpt = GL_REPEAT;
1203 enum filterOpt = GL_NEAREST; //GL_LINEAR;
1204 enum ttype = GL_UNSIGNED_BYTE;
1206 glGenTextures(1, &zxtexid);
1207 if (zxtexid == 0) assert(0, "can't create OpenGL texture");
1209 //GLint gltextbinding;
1210 //glGetIntegerv(GL_TEXTURE_BINDING_2D, &gltextbinding);
1211 //scope(exit) glBindTexture(GL_TEXTURE_2D, gltextbinding);
1213 glBindTexture(GL_TEXTURE_2D, zxtexid);
1214 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapOpt);
1215 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapOpt);
1216 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filterOpt);
1217 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filterOpt);
1218 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
1219 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
1221 GLfloat[4] bclr = 0.0;
1222 glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, bclr.ptr);
1224 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, VBufWidth, VBufHeight, 0, GLTexType, GL_UNSIGNED_BYTE, zxtexbuf.ptr);
1227 createArrowTexture();
1229 glconInit(winWidthScaled, winHeightScaled);
1230 vbwin.redrawOpenGlSceneNow();
1232 updateThreadId = spawn(&updateThread, thisTid);
1235 postScreenUpdate();
1236 repostHideMouse();
1237 foreach (Group g; groups) g.forceUpdated();
1239 vbwin.eventLoop(1000*10,
1240 delegate () {
1241 if (vbwin.closed) return;
1242 if (isQuitRequested) { vbwin.close(); return; }
1243 bool needUpdate = false;
1244 foreach (Group g; groups) if (g.needUpdate()) { needUpdate = true; break; }
1245 if (needUpdate) {
1246 //vbwinLocked = true;
1247 updateThreadId.send(UpThreadCommand.StartUpdate);
1248 //glUpdateTexture();
1249 //vbwin.redrawOpenGlSceneNow();
1251 if (!conQueueEmpty()) postDoConCommands();
1253 delegate (KeyEvent event) {
1254 if (vbwin.closed) return;
1255 if (isQuitRequested) { vbwin.close(); return; }
1256 scope(exit) {
1257 if (!conQueueEmpty()) postDoConCommands();
1259 if (glconKeyEvent(event)) {
1260 postScreenRepaint();
1261 return;
1263 if (event.pressed && event == "C-Q") { concmd("quit"); return; }
1264 if (subwins.length) {
1265 if (!vbwinLocked || subwins[$-1].immuneToLock) {
1266 if (event.pressed) ignoreSubWinChar = false;
1267 subwins[$-1].onKey(event);
1268 postScreenUpdate();
1269 } else {
1270 ignoreSubWinChar = false;
1272 return;
1274 if (event.pressed) {
1275 //conwriteln("key: ", event.toStr, ": ", event.modifierState&ModifierState.windows);
1276 if (event == "N") { concmd("next_unread ona"); return; }
1277 if (event == "S-N") { concmd("next_unread tan"); return; }
1278 if (event == "U") { concmd("mark_unread"); return; }
1279 if (event == "C-R") { concmd("mark_read"); return; }
1280 if (event == "Space") { concmd("artext_page_down"); return; }
1281 if (event == "S-Space") { concmd("artext_page_up"); return; }
1282 if (event == "Up" || event == "Pad8") { concmd("article_prev"); return; }
1283 if (event == "Down" || event == "Pad2") { concmd("article_next"); return; }
1284 if (event == "PageUp" || event == "Pad9") { concmd("article_pgup"); return; }
1285 if (event == "PageDown" || event == "Pad3") { concmd("article_pgdown"); return; }
1286 if (event == "Home" || event == "Pad7") { concmd("article_to_first"); return; }
1287 if (event == "End" || event == "Pad1") { concmd("article_to_last"); return; }
1288 if (event == "C-Up" || event == "C-Pad8") { concmd("article_scroll_up"); return; }
1289 if (event == "C-Down" || event == "C-Pad2") { concmd("article_scroll_down"); return; }
1290 if (event == "C-PageUp" || event == "C-Pad9") { concmd("group_prev"); return; }
1291 if (event == "C-PageDown" || event == "C-Pad3") { concmd("group_next"); return; }
1292 if (event == "C-M-U") { concmd("group_update"); return; }
1293 if (event == "C-H") { concmd("article_dump_headers"); return; }
1294 if (event == "C-S-I") { concmd("update_all"); return; }
1295 if (event == "C-Insert") { concmd("find_mine"); return; }
1296 if (event == "T") {
1297 if (auto g = getActiveGroup) {
1298 g.withBase(delegate (abase) {
1299 if (g.curidx < g.length) {
1300 if (auto art = abase[g.baseidx(g.curidx)]) {
1301 auto t = twits.check(*art);
1302 if (t) {
1303 new TitlePrompt(art.fromname~" <"~art.frommail~">", t.name, art.msgid, t.title);
1304 } else {
1305 new TitlePrompt(art.fromname~" <"~art.frommail~">", art.fromname~" <"~art.frommail~">", art.msgid, "");
1311 return;
1313 // twit thread
1314 if (event == "C-M-K") {
1315 if (auto g = getActiveGroup) {
1316 g.withBase(delegate (abase) {
1317 if (g.curidx < g.length) {
1318 uint ridx = g.baseidx(g.curidx);
1319 if (auto art = abase[ridx]) {
1320 while (abase[ridx].parent != 0) ridx = abase[ridx].parent;
1321 threadtwits.add(art.msgid);
1322 g.moveToNextThread();
1323 void doMark (uint idx) {
1324 while (idx != 0) {
1325 auto a = abase[idx];
1326 if (a is null) break;
1327 if (a.unread) {
1328 a.unread = false;
1329 a.updated = true;
1331 doMark(a.firstchild);
1332 idx = a.nextsib;
1335 doMark(art.firstchild);
1336 g.buildVisibleList();
1340 postScreenUpdate();
1342 return;
1344 if (event == "R") {
1345 if (auto g = getActiveGroup) {
1346 g.withBase((abase) {
1347 if (g.curidx < g.length) {
1348 abase.loadContent(g.baseidx(g.curidx));
1349 if (auto art = abase[g.baseidx(g.curidx)]) {
1350 new PostEditor(g.groupname, *art);
1355 return;
1357 if (event == "S-P") {
1358 if (auto g = getActiveGroup) {
1359 g.withBase((abase) {
1360 new PostEditor(g.groupname, Article());
1363 return;
1365 if (event == "S-Enter") {
1366 if (auto g = getActiveGroup) {
1367 g.withBase((abase) {
1368 if (g.curidx < g.length) {
1369 if (auto art = abase[g.baseidx(g.curidx)]) {
1370 import std.stdio : File;
1371 import std.process;
1372 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
1373 string id = art.msgid;
1374 if (id.length > 2 && id[0] == '<' && id[$-1] == '>') id = id[1..$-1];
1375 spawnProcess(
1376 ["opera", "http://forum.dlang.org/post/"~id],
1377 File("/dev/zero"), File("/dev/null"), File("/dev/null"),
1383 return;
1386 postScreenRepaint();
1388 delegate (MouseEvent event) {
1389 if (vbwin.closed) return;
1390 scope(exit) {
1391 if (!conQueueEmpty()) postDoConCommands();
1393 oldMouseX = event.x;
1394 oldMouseY = event.y;
1395 mouseMoved();
1396 if (subwins.length) {
1397 auto win = subwins[$-1];
1398 // start drag?
1399 if (!subwinDrag && event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
1400 int mx = event.x/vbufEffScale;
1401 int my = event.y/vbufEffScale;
1402 if (mx >= win.winx && my >= win.winy && mx < win.winx+win.winw && my < win.winy+10) {
1403 subwinDrag = true;
1404 subwinDragXSpot = win.winx-mx;
1405 subwinDragYSpot = win.winy-my;
1406 postScreenUpdate();
1407 return;
1410 // draw
1411 if (subwinDrag) {
1412 win.winx = event.x/vbufEffScale+subwinDragXSpot;
1413 win.winy = event.y/vbufEffScale+subwinDragYSpot;
1415 // stop drag?
1416 if (subwinDrag && event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
1417 subwinDrag = false;
1418 postScreenUpdate();
1419 return;
1421 if (!vbwinLocked || win.immuneToLock) win.onMouse(event);
1422 postScreenUpdate();
1423 return;
1425 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
1426 int mx = event.x/vbufEffScale;
1427 int my = event.y/vbufEffScale;
1428 // select group
1429 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < groups.length*10) {
1430 if (auto g = getActiveGroup) g.active = false;
1431 groups[my/10].active = true;
1432 postScreenUpdate();
1433 return;
1435 // select post
1436 if (mx > guiGroupListWidth && mx < VBufWidth && my >= 0 && my < guiThreadListHeight) {
1437 if (auto g = getActiveGroup) {
1438 my -= g.ytopofs;
1439 my /= gxTextHeightUtf;
1440 g.curidx = g.msgtop+my;
1441 postScreenUpdate();
1442 return;
1446 // wheel
1447 if (event.type == MouseEventType.buttonPressed && (event.button == MouseButton.wheelUp || event.button == MouseButton.wheelDown)) {
1448 int mx = event.x/vbufEffScale;
1449 int my = event.y/vbufEffScale;
1450 // group
1451 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < VBufHeight) {
1452 if (groups.length > 0) {
1453 int gi = getActiveGroupIndex();
1454 gi += (event.button == MouseButton.wheelUp ? -1 : 1);
1455 if (gi < 0) gi = 0;
1456 if (gi >= groups.length) gi = cast(int)groups.length-1;
1457 if (auto g = getActiveGroup) g.active = false;
1458 groups[gi].active = true;
1459 postScreenUpdate();
1461 return;
1463 // select post
1464 if (mx > guiGroupListWidth && mx < VBufWidth && my >= 0 && my < guiThreadListHeight) {
1465 if (auto g = getActiveGroup) {
1466 if (event.button == MouseButton.wheelUp) g.moveUp(); else g.moveDown();
1467 postScreenUpdate();
1468 return;
1472 postScreenRepaint();
1474 delegate (dchar ch) {
1475 if (vbwin.closed) return;
1476 scope(exit) {
1477 if (!conQueueEmpty()) postDoConCommands();
1479 if (glconCharEvent(ch)) {
1480 postScreenRepaint();
1481 return;
1483 if (subwins.length) {
1484 if (!vbwinLocked || subwins[$-1].immuneToLock) {
1485 if (!ignoreSubWinChar) subwins[$-1].onChar(ch);
1486 ignoreSubWinChar = false;
1487 postScreenUpdate();
1488 } else {
1489 ignoreSubWinChar = false;
1491 return;
1495 updateThreadId.send(UpThreadCommand.Quit);