sqlite: fixed thread relinking logic; fixed individual thread relinking API (it actua...
[chiroptera.git] / chiroptera.d
blobdcf3c4fadf0d4641fdf00db1959d367fe52316cf
1 /* E-Mail Client
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, version 3 of the License ONLY.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module chiroptera /*is aliced*/;
19 import core.atomic;
20 import core.time;
21 import std.concurrency;
23 //import arsd.email;
24 //import arsd.htmltotext;
25 import arsd.simpledisplay;
26 import arsd.png;
28 import iv.alice;
29 import iv.bclamp;
30 import iv.encoding;
31 import iv.cmdcon;
32 import iv.cmdcongl;
33 import iv.lockfile;
34 import iv.sdpyutil;
35 import iv.strex;
36 import iv.utfutil;
37 import iv.vfs.io;
39 import egfx;
40 import egui;
41 import maildb;
43 import account;
44 import addrbook;
45 import chievents;
46 import folder;
47 import hitwit;
49 import dialogs;
52 // ////////////////////////////////////////////////////////////////////////// //
53 __gshared ubyte vbufNewScale = 1; // new window scale
54 __gshared int lastWinWidth, lastWinHeight;
55 __gshared bool vbufVSync = false;
56 __gshared bool vbufEffVSync = false;
58 __gshared NotificationAreaIcon trayicon;
59 __gshared Image[6] trayimages;
60 __gshared MemoryImage[6] icons; // 0: normal
63 // ////////////////////////////////////////////////////////////////////////// //
64 static immutable ubyte[] iconsZipData = cast(immutable(ubyte)[])import("databin/icons.zip");
67 // ////////////////////////////////////////////////////////////////////////// //
68 struct MailReplacement {
69 string oldmail;
70 string newmail;
71 string newname;
74 __gshared MailReplacement[string] repmailreps;
77 // ////////////////////////////////////////////////////////////////////////// //
78 string getBrowserCommand (bool forceOpera) {
79 __gshared string browser;
80 if (forceOpera) return "opera";
81 if (browser.length == 0) {
82 import core.stdc.stdlib : getenv;
83 const(char)* evar = getenv("BROWSER");
84 if (evar !is null && evar[0]) {
85 import std.string : fromStringz;
86 browser = evar.fromStringz.idup;
87 } else {
88 browser = "opera";
91 return browser;
95 // ////////////////////////////////////////////////////////////////////////// //
96 __gshared bool dbg_dump_keynames;
99 // ////////////////////////////////////////////////////////////////////////// //
100 class TrayAnimationStepEvent {}
101 __gshared TrayAnimationStepEvent evTrayAnimationStep;
102 shared static this () { evTrayAnimationStep = new TrayAnimationStepEvent(); }
104 __gshared Tid updateThreadId;
105 shared bool updateInProgress = false;
106 __gshared int trayAnimationIndex = 0; // 0: no animation
107 __gshared int trayAnimationDir = 1; // direction
110 // ////////////////////////////////////////////////////////////////////////// //
111 void trayPostAnimationEvent () {
112 if (vbwin !is null && !vbwin.eventQueued!TrayAnimationStepEvent) vbwin.postTimeout(evTrayAnimationStep, 100);
116 void trayDoAnimationStep () {
117 if (trayicon is null || trayicon.closed) return; // no tray icon
118 if (vbwin is null || vbwin.closed) return;
119 if (trayAnimationIndex == 0) return; // no animation
120 trayPostAnimationEvent();
121 if (trayAnimationDir < 0) {
122 if (--trayAnimationIndex == 1) trayAnimationDir = 1;
123 } else {
124 if (++trayAnimationIndex == trayimages.length-1) trayAnimationDir = -1;
126 trayicon.icon = trayimages[trayAnimationIndex];
127 //vbwin.icon = icons[trayAnimationIndex];
128 //vbwin.sendDummyEvent(); // or it won't redraw itself
129 //flushGui(); // or it may not redraw itself
133 // ////////////////////////////////////////////////////////////////////////// //
134 void trayStartAnimation () {
135 if (trayicon is null) return; // no tray icon
136 if (trayAnimationIndex == 0) {
137 trayAnimationIndex = 1;
138 trayAnimationDir = 1;
139 trayicon.icon = trayimages[1];
140 vbwin.icon = icons[1];
141 //vbwin.sendDummyEvent(); // or it may not redraw itself
142 flushGui(); // or it may not redraw itself
143 trayPostAnimationEvent();
148 void trayStopAnimation () {
149 if (trayicon is null) return; // no tray icon
150 if (trayAnimationIndex != 0) {
151 trayAnimationIndex = 0;
152 trayAnimationDir = 1;
153 trayicon.icon = trayimages[0];
154 vbwin.icon = icons[0];
155 //vbwin.sendDummyEvent(); // or it may not redraw itself
156 flushGui(); // or it may not redraw itself
161 // check if we have to start/stop animation, and do it
162 void setupTrayAnimation () {
163 foreach (Folder fld; folders) {
164 if (fld.unreadCount > 0) { trayStartAnimation(); return; }
166 trayStopAnimation();
170 // ////////////////////////////////////////////////////////////////////////// //
171 __gshared string[string] mainAppKeyBindings;
173 void clearBindings () {
174 mainAppKeyBindings.clear();
178 void mainAppBind (ConString kname, ConString concmd) {
179 KeyEvent evt = KeyEvent.parse(kname);
180 if (concmd.length) {
181 mainAppKeyBindings[evt.toStr] = concmd.idup;
182 } else {
183 mainAppKeyBindings.remove(evt.toStr);
188 void mainAppUnbind (ConString kname) {
189 KeyEvent evt = KeyEvent.parse(kname);
190 mainAppKeyBindings.remove(evt.toStr);
194 void setupDefaultBindings () {
195 //mainAppBind("C-L", "dbg_font_window");
196 mainAppBind("C-Q", "quit_prompt");
197 mainAppBind("N", "next_unread ona");
198 mainAppBind("S-N", "next_unread tan");
199 mainAppBind("M", "next_unread ona");
200 mainAppBind("S-M", "next_unread tan");
201 mainAppBind("U", "mark_unread");
202 mainAppBind("R", "mark_read");
203 mainAppBind("Space", "artext_page_down");
204 mainAppBind("S-Space", "artext_page_up");
205 mainAppBind("M-Up", "artext_line_up");
206 mainAppBind("M-Down", "artext_line_down");
207 mainAppBind("Up", "article_prev");
208 mainAppBind("Down", "article_next");
209 mainAppBind("PageUp", "article_pgup");
210 mainAppBind("PageDown", "article_pgdown");
211 mainAppBind("Home", "article_to_first");
212 mainAppBind("End", "article_to_last");
213 mainAppBind("C-Up", "article_scroll_up");
214 mainAppBind("C-Down", "article_scroll_down");
215 mainAppBind("C-PageUp", "folder_prev");
216 mainAppBind("C-PageDown", "folder_next");
217 mainAppBind("C-M-U", "folder_update");
218 mainAppBind("C-H", "article_dump_headers");
219 mainAppBind("C-S-I", "update_all");
220 mainAppBind("C-Backslash", "find_mine");
221 mainAppBind("C-Slash", "article_to_parent");
222 mainAppBind("C-Comma", "article_to_prev_sib");
223 mainAppBind("C-Period", "article_to_next_sib");
224 mainAppBind("C-Insert", "article_copy_url_to_clipboard");
225 mainAppBind("C-M-K", "article_twit_thread");
226 mainAppBind("T", "article_edit_poster_title");
227 mainAppBind("C-R", "article_reply");
228 mainAppBind("S-R", "article_reply_to_from");
229 mainAppBind("S-P", "new_post");
230 mainAppBind("S-Enter", "article_open_in_browser");
231 mainAppBind("M-Enter", "article_open_in_browser tan");
232 mainAppBind("Delete", "article_softdelete_toggle");
236 // ////////////////////////////////////////////////////////////////////////// //
237 enum UpThreadCommand {
238 Ping,
239 StartUpdate, // start updating now
240 Quit,
243 void updateThread (Tid ownerTid) {
244 import core.time;
245 bool doQuit = false;
246 try {
247 MonoTime lastCollect = MonoTime.currTime;
248 while (!doQuit) {
249 receiveTimeout(30.seconds,
250 (UpThreadCommand cmd) {
251 final switch (cmd) {
252 case UpThreadCommand.Ping: break;
253 case UpThreadCommand.StartUpdate: break;
254 case UpThreadCommand.Quit: doQuit = true; break;
258 if (doQuit) break;
259 bool updateProgressSet = false;
260 foreach (Account acc; accounts) {
261 if (acc.needUpdate) {
262 updateProgressSet = true;
263 atomicStore(updateInProgress, true);
264 try {
265 if (vbwin !is null) vbwin.postEvent(new UpdatingAccountEvent(acc.name));
266 acc.update();
267 } catch (Exception e) {
268 conwriteln("ERROR UPDATING ACCOUNT '", acc.name, "': ", e.msg);
269 string etrc;
270 e.toString(delegate (in char[] c) { etrc ~= c; });
271 conwriteln("---");
272 conwriteln(etrc);
274 if (vbwin !is null) vbwin.postEvent(new UpdatingAccountCompleteEvent(acc.name));
277 if (updateProgressSet) {
278 if (vbwin !is null) vbwin.postEvent(new UpdatingCompleteEvent());
279 atomicStore(updateInProgress, false);
282 auto ctt = MonoTime.currTime;
283 if ((ctt-lastCollect).total!"minutes" >= 5) {
284 import core.memory : GC;
285 lastCollect = ctt;
286 GC.collect();
287 GC.minimize();
291 } catch (Throwable e) {
292 // here, we are dead and fucked (the exact order doesn't matter)
293 import core.stdc.stdlib : abort;
294 import core.stdc.stdio : fprintf, stderr;
295 import core.memory : GC;
296 import core.thread : thread_suspendAll;
297 GC.disable(); // yeah
298 thread_suspendAll(); // stop right here, you criminal scum!
299 auto s = e.toString();
300 fprintf(stderr, "\n=== FATAL ===\n%.*s\n", cast(uint)s.length, s.ptr);
301 abort(); // die, you bitch!
306 // ////////////////////////////////////////////////////////////////////////// //
307 void initConsole () {
308 import std.functional : toDelegate;
310 //conRegVar!vbufNewScale(1, 4, "v_scale", "window scale: [1..3]");
312 conRegVar!bool("v_vsync", "sync to video refresh rate?",
313 (ConVarBase self) => vbufVSync,
314 (ConVarBase self, bool nv) {
315 static if (EGfxOpenGLBackend) {
316 if (vbufVSync != nv) {
317 vbufVSync = nv;
318 postScreenRepaint();
324 conRegVar!scrollKeepLines("scroll_keep_lines", "number of lines to keep on page up or page down.");
325 conRegVar!unreadTimeoutInterval("t_unread_timeout", "timout to mark keyboard-selected message as unread (<1: don't mark)");
327 conRegFunc!clearBindings("binds_app_clear", "clear main application keybindings");
328 conRegFunc!setupDefaultBindings("binds_app_default", "*append* default application bindings");
329 conRegFunc!mainAppBind("bind_app", "add main application binding");
330 conRegFunc!mainAppUnbind("unbind_app", "remove main application binding");
333 // //////////////////////////////////////////////////////////////////// //
334 conRegFunc!(() {
335 import core.memory : GC;
336 conwriteln("starting GC collection...");
337 GC.collect();
338 GC.minimize();
339 conwriteln("GC collection complete.");
340 })("gc_collect", "force GC collection cycle");
343 // //////////////////////////////////////////////////////////////////// //
344 conRegFunc!(() {
345 auto qww = new YesNoWindow("Quit?", "Do you really want to quit?", true);
346 qww.onYes = () { concmd("quit"); };
347 qww.addModal();
348 })("quit_prompt", "quit with prompt");
351 // //////////////////////////////////////////////////////////////////// //
352 conRegFunc!((ConString url, bool forceOpera=false) {
353 if (url.length) {
354 import std.stdio : File;
355 import std.process;
356 try {
357 auto frd = File("/dev/null");
358 auto fwr = File("/dev/null", "w");
359 spawnProcess([getBrowserCommand(forceOpera), url.idup], frd, fwr, fwr, null, Config.detached);
360 } catch (Exception e) {
361 conwriteln("ERROR executing URL viewer (", e.msg, ")");
364 })("open_url", "open given url in a browser");
367 // //////////////////////////////////////////////////////////////////// //
368 conRegFunc!(() {
369 foreach (Account acc; accounts) acc.forceUpdating();
370 if (!atomicLoad(updateInProgress)) {
371 updateThreadId.send(UpThreadCommand.StartUpdate);
373 })("update_all", "mark all groups for updating");
376 // //////////////////////////////////////////////////////////////////// //
377 conRegFunc!(() {
378 if (folderCur > 0) {
379 folderCur = folderCur-1;
380 if (mainPane !is null) mainPane.resetUnreadTimer();
382 postScreenRebuild();
383 })("folder_prev", "go to previous group");
385 conRegFunc!(() {
386 if (folders.length-folderCur > 1) {
387 folderCur = folderCur+1;
388 if (mainPane !is null) mainPane.resetUnreadTimer();
390 postScreenRebuild();
391 })("folder_next", "go to next group");
394 // //////////////////////////////////////////////////////////////////// //
395 conRegFunc!(() {
396 if (auto fld = getActiveFolder) {
397 if (fld.markAsUnread()) {
398 if (mainPane !is null) mainPane.resetUnreadTimer();
399 postScreenRebuild();
402 })("mark_unread", "mark current message as unread");
404 conRegFunc!(() {
405 if (auto fld = getActiveFolder) {
406 if (fld.markAsRead()) {
407 if (mainPane !is null) mainPane.resetUnreadTimer();
408 postScreenRebuild();
411 })("mark_read", "mark current message as read");
413 conRegFunc!((bool allowNextGroup=false) {
414 if (auto fld = getActiveFolder) {
415 if (!fld.moveToNextUnread(true)) {
416 if (!allowNextGroup) return;
417 // try other folders
418 uint fidx = cast(uint)((folderCur+1)%folders.length);
419 foreach (immutable _; 0..folders.length) {
420 if (folders[fidx].moveToNextUnread(true)) {
421 folderCur = fidx;
422 if (mainPane !is null) mainPane.resetUnreadTimer();
423 postScreenRebuild();
424 return;
426 fidx = cast(uint)((fidx+1)%folders.length);
428 return;
430 postScreenRebuild();
432 })("next_unread", "move to next unread message; bool arg: can skip to next group?");
435 // //////////////////////////////////////////////////////////////////// //
436 conRegFunc!(() {
437 if (mainPane !is null) mainPane.scrollByPageUp();
438 })("artext_page_up", "do pageup on article text");
440 conRegFunc!(() {
441 if (mainPane !is null) mainPane.scrollByPageDown();
442 })("artext_page_down", "do pagedown on article text");
444 conRegFunc!(() {
445 if (mainPane !is null) mainPane.scrollBy(-1);
446 })("artext_line_up", "do lineup on article text");
448 conRegFunc!(() {
449 if (mainPane !is null) mainPane.scrollBy(1);
450 })("artext_line_down", "do linedown on article text");
452 // //////////////////////////////////////////////////////////////////// //
453 conRegFunc!((bool forceOpera=false) {
454 if (auto fldx = getActiveFolder) {
455 fldx.withBaseReader((abase, cur, top, alist) {
456 if (cur < alist.length) {
457 abase.loadContent(alist[cur]);
458 if (auto art = abase[alist[cur]]) {
459 scope(exit) art.releaseContent();
460 auto path = art.getHeaderValue("path:");
461 //conwriteln("path: [", path, "]");
462 if (path.startsWithCI("digitalmars.com!.POSTED.")) {
463 import std.stdio : File;
464 import std.process;
465 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
466 string id = art.msgid;
467 if (id.length > 2 && id[0] == '<' && id[$-1] == '>') id = id[1..$-1];
468 auto pid = spawnProcess(
469 [getBrowserCommand(forceOpera), "http://forum.dlang.org/post/"~id],
470 File("/dev/zero"), File("/dev/null"), File("/dev/null"),
472 pid.wait();
478 })("article_open_in_browser", "open the current article in browser (dlang forum)");
480 conRegFunc!(() {
481 if (auto fldx = getActiveFolder) {
482 fldx.withBaseReader((abase, cur, top, alist) {
483 if (cur < alist.length) {
484 if (auto art = abase[alist[cur]]) {
485 auto path = art.getHeaderValue("path:");
486 if (path.startsWithCI("digitalmars.com!.POSTED.")) {
487 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
488 string id = art.msgid;
489 if (id.length > 2 && id[0] == '<' && id[$-1] == '>') id = id[1..$-1];
490 id = "http://forum.dlang.org/post/"~id;
491 setClipboardText(vbwin, id);
492 setPrimarySelection(vbwin, id);
498 })("article_copy_url_to_clipboard", "copy the url of the current article to clipboard (dlang forum)");
501 // //////////////////////////////////////////////////////////////////// //
502 conRegFunc!((ConString oldmail, ConString newmail, ConString newname) {
503 if (oldmail.length) {
504 if (newmail.length == 0) {
505 repmailreps.remove(oldmail.idup);
506 } else {
507 MailReplacement mr;
508 mr.oldmail = oldmail.idup;
509 mr.newmail = newmail.idup;
510 mr.newname = newname.idup;
511 repmailreps[mr.oldmail] = mr;
514 })("append_replyto_mail_replacement", "append replacement for reply mails");
517 // //////////////////////////////////////////////////////////////////// //
518 conRegFunc!(() {
519 if (auto fld = getActiveFolder) {
520 fld.moveUp();
521 if (mainPane !is null) mainPane.setupUnreadTimer();
522 postScreenRebuild();
524 })("article_prev", "go to previous article");
526 conRegFunc!(() {
527 if (auto fld = getActiveFolder) {
528 fld.moveDown();
529 if (mainPane !is null) mainPane.setupUnreadTimer();
530 postScreenRebuild();
532 })("article_next", "go to next article");
534 conRegFunc!(() {
535 if (auto fld = getActiveFolder) {
536 fld.movePageUp();
537 if (mainPane !is null) mainPane.setupUnreadTimer();
538 postScreenRebuild();
540 })("article_pgup", "artiles list: page up");
542 conRegFunc!(() {
543 if (auto fld = getActiveFolder) {
544 fld.movePageDown();
545 if (mainPane !is null) mainPane.setupUnreadTimer();
546 postScreenRebuild();
548 })("article_pgdown", "artiles list: page down");
550 conRegFunc!(() {
551 if (auto fld = getActiveFolder) {
552 fld.scrollUp();
553 if (mainPane !is null) mainPane.setupUnreadTimer();
554 postScreenRebuild();
556 })("article_scroll_up", "scroll article list up");
558 conRegFunc!(() {
559 if (auto fld = getActiveFolder) {
560 fld.scrollDown();
561 if (mainPane !is null) mainPane.setupUnreadTimer();
562 postScreenRebuild();
564 })("article_scroll_down", "scroll article list up");
566 conRegFunc!(() {
567 if (auto fld = getActiveFolder) {
568 fld.moveToFirst();
569 if (mainPane !is null) mainPane.setupUnreadTimer();
570 postScreenRebuild();
572 })("article_to_first", "go to first article");
574 conRegFunc!(() {
575 if (auto fld = getActiveFolder) {
576 fld.moveToLast();
577 if (mainPane !is null) mainPane.setupUnreadTimer();
578 postScreenRebuild();
580 })("article_to_last", "go to last article");
583 // //////////////////////////////////////////////////////////////////// //
584 conRegFunc!(() {
585 if (auto fld = getActiveFolder) {
586 auto postDg = delegate (Account acc) {
587 conwriteln("post with account '", acc.name, "' (", acc.mail, ")");
588 auto pw = new PostWindow();
589 pw.from.str = acc.realname~" <"~acc.mail~">";
590 pw.from.readonly = true;
591 if (auto nna = cast(NntpAccount)acc) {
592 pw.title = "Reply to NNTP "~nna.server~":"~nna.group;
593 pw.to.str = nna.group;
594 pw.to.readonly = true;
595 pw.activeWidget = pw.subj;
596 } else {
597 pw.title = "Reply from '"~acc.name~"' ("~acc.mail~")";
598 pw.to.str = "";
599 pw.activeWidget = pw.to;
601 pw.subj.str = "";
602 pw.acc = acc;
603 pw.fld = fld;
606 auto acc = fld.findAccountToPost();
607 if (acc is null) {
608 auto wacc = new SelectPopBoxWindow(defaultAcc);
609 wacc.onSelected = postDg;
610 } else {
611 postDg(acc);
612 acc = defaultAcc;
614 } else {
615 conwriteln("post: no active folder");
617 })("new_post", "post a new article or message");
620 // //////////////////////////////////////////////////////////////////// //
621 static string buildToStr (string name, string mail, ref string newfromname) {
622 if (auto mrp = mail in repmailreps) {
623 newfromname = mrp.newname;
624 return mrp.newname~" <"~mrp.newmail~">";
625 } else {
626 return name~" <"~mail~">";
630 static void doReply (string repfld) {
631 if (auto fld = getActiveFolder) {
632 auto postDg = delegate (Account acc) {
633 conwriteln("reply with account '", acc.name, "' (", acc.mail, ")");
634 fld.withBaseReader((abase, cur, top, alist) {
635 if (cur < alist.length) {
636 auto aidx = alist.ptr[cur];
637 abase.loadContent(aidx);
638 if (auto art = abase[aidx]) {
639 assert(art.contentLoaded);
640 auto atext = art.getTextContent;
641 auto pw = new PostWindow();
642 pw.from.str = acc.realname~" <"~acc.mail~">";
643 pw.from.readonly = true;
644 string from = art.fromname;
646 if (auto nna = cast(NntpAccount)acc) {
647 pw.title = "Reply to NNTP "~nna.server~":"~nna.group;
648 pw.to.str = nna.group;
649 pw.to.readonly = true;
650 } else {
651 pw.title = "Reply from '"~acc.name~"' ("~acc.mail~")";
652 //pw.to.str = art.fromname~" <"~art.frommail~">";
653 string rs;
654 if (repfld != "From") {
655 auto rf = art.getHeaderValue(repfld);
656 if (rf) {
657 //rs = buildToStr(art.fromname, repfld, from);
658 if (art.getHeaderValue("List-Id").length) {
659 rs = rf.idup;
660 } else {
661 rs = art.fromname~" <"~rf.idup~">";
665 if (!rs) rs = buildToStr(art.fromname, art.frommail, from);
666 pw.to.str = rs;
668 pw.subj.str = "Re: "~art.subj;
671 auto vp = from.indexOf(" via Digitalmars-");
672 if (vp > 0) {
673 from = from[0..vp].xstrip;
674 if (from.length == 0) from = "anonymous";
678 pw.ed.addText(from);
679 pw.ed.addText(" wrote:\n");
680 pw.ed.addText("\n");
681 foreach (ConString s; LineIterator!false(atext)) {
682 if (s.length == 0 || s[0] == '>') pw.ed.addText(">"); else pw.ed.addText("> ");
683 pw.ed.addText(s);
684 pw.ed.addText("\n");
686 pw.ed.addText("\n");
687 pw.ed.reformat();
688 pw.replyto = art.msgid;
689 pw.references = art.getHeaderValue("References").xstrip.idup;
690 pw.acc = acc;
691 pw.fld = fld;
692 pw.activeWidget = pw.ed;
698 auto acc = fld.findAccountToPost(fld.curidx);
699 if (acc is null) {
700 auto wacc = new SelectPopBoxWindow(defaultAcc);
701 wacc.onSelected = postDg;
702 } else {
703 postDg(acc);
704 acc = defaultAcc;
709 conRegFunc!(() { doReply("Reply-To"); })("article_reply", "reply to the current article with \"reply-to\" field");
710 conRegFunc!(() { doReply("From"); })("article_reply_to_from", "reply to the current article with \"from\" field");
713 // //////////////////////////////////////////////////////////////////// //
714 conRegFunc!(() {
715 if (auto fld = getActiveFolder) {
716 fld.withBaseReader((abase, cur, top, alist) {
717 if (cur < alist.length) {
718 auto aidx = alist.ptr[cur];
719 abase.loadContent(aidx);
720 if (auto art = abase[aidx]) {
721 assert(art.contentLoaded);
722 articleBogoMarkHam(art);
723 //TODO: move out of spam
728 })("article_mark_ham", "mark current article as ham");
731 conRegFunc!(() {
732 if (auto fld = getActiveFolder) {
733 Article newart;
734 uint didx = uint.max;
735 Folder fldspam = getSpamFolder;
736 fld.withBaseReader((abase, cur, top, alist) {
737 if (cur < alist.length) {
738 auto aidx = alist.ptr[cur];
739 abase.loadContent(aidx);
740 if (auto art = abase[aidx]) {
741 assert(art.contentLoaded);
742 conwriteln("marking ", art.msgid, " as spam");
743 articleBogoMarkSpam(art);
744 if (fld !is fldspam) {
745 conwriteln(" should be moved to spam folder");
746 newart = art.clone();
747 didx = aidx;
752 // delete it
753 if (didx != uint.max) {
754 assert(newart !is null);
755 conwriteln("removing ", newart.msgid, " from ", fld.folderPath);
756 fld.withBaseWriter((abase) {
757 abase.softDeleted(didx, true);
758 abase.writeUpdates();
760 fld.markForRebuild();
761 fld.buildVisibleList();
763 // insert into spam
764 if (newart !is null && fldspam !is null) {
765 newart.unread = false;
766 conwriteln("adding ", newart.msgid, " to ", fldspam.folderPath);
767 fldspam.withBaseWriter((abase) {
768 abase.insert(newart);
769 abase.writeUpdates();
771 newart.releaseContent();
772 fldspam.markForRebuild();
773 fldspam.buildVisibleList();
775 postScreenRebuild();
777 })("article_mark_spam", "mark current article as spam");
780 // //////////////////////////////////////////////////////////////////// //
781 conRegFunc!((ConString fldname) {
782 if (auto fld = getActiveFolder) {
783 Folder destfld = findFolderByPath(fldname);
784 if (destfld is null) { conwriteln("cannot find folder '", fldname, "'"); }
785 if (destfld is fld) { conwriteln("cannot move to the same folder"); return; }
786 Article newart;
787 uint didx = uint.max;
788 fld.withBaseReader((abase, cur, top, alist) {
789 if (cur < alist.length) {
790 auto aidx = alist.ptr[cur];
791 abase.loadContent(aidx);
792 if (auto art = abase[aidx]) {
793 assert(art.contentLoaded);
794 newart = art.clone();
795 didx = aidx;
799 if (didx == uint.max || newart is null) { conwriteln("article not found!"); return; }
800 // delete it
801 assert(didx != uint.max);
802 assert(newart !is null);
803 conwriteln("removing ", newart.msgid, " from ", fld.folderPath);
804 fld.withBaseWriter((abase) {
805 abase.softDeleted(didx, true);
806 abase.writeUpdates();
808 fld.markForRebuild();
809 fld.buildVisibleList();
810 // insert into spam
811 conwriteln("adding ", newart.msgid, " to ", destfld.folderPath);
812 destfld.withBaseWriter((abase) {
813 abase.insert(newart);
814 abase.writeUpdates();
816 newart.releaseContent();
817 destfld.markForRebuild();
818 destfld.buildVisibleList();
819 postScreenRebuild();
821 })("article_move_to_folder", "move article to existing folder");
824 // //////////////////////////////////////////////////////////////////// //
825 conRegFunc!(() {
826 if (auto fld = getActiveFolder) {
827 fld.moveToParent();
828 if (mainPane !is null) mainPane.setupUnreadTimer();
829 postScreenRebuild();
831 })("article_to_parent", "jump to parent article, if any");
833 conRegFunc!(() {
834 if (auto fld = getActiveFolder) {
835 fld.moveToPrevSib();
836 if (mainPane !is null) mainPane.setupUnreadTimer();
837 postScreenRebuild();
839 })("article_to_prev_sib", "jump to previous sibling");
841 conRegFunc!(() {
842 if (auto fld = getActiveFolder) {
843 fld.moveToNextSib();
844 if (mainPane !is null) mainPane.setupUnreadTimer();
845 postScreenRebuild();
847 })("article_to_next_sib", "jump to next sibling");
850 // //////////////////////////////////////////////////////////////////// //
851 conRegFunc!(() {
852 if (auto fld = getActiveFolder) {
853 if (auto acc = findNntpAccountForFolder(fld)) {
854 fld.withBaseReader(delegate (abase, cur, top, alist) {
855 if (cur < alist.length) {
856 if (auto art = abase[alist[cur]]) {
857 auto t = fld.isTwittedNL(cur);
858 auto setdg = delegate (string name, string mail, string folder, string title) {
859 string xcmd;
860 if (t is null) {
861 if (title.length != 0) {
862 concmdfdg("twit_set name \"%s\" mail \"%s\" folder_mask \"%s\" title \"%s\"", (ConString cmd) { xcmd = cmd.xstrip.idup; }, name, mail, folder, title);
864 } else {
865 if (title.length == 0) {
866 concmdfdg("twit_unset name \"%s\" mail \"%s\" folder_mask \"%s\"", (ConString cmd) { xcmd = cmd.xstrip.idup; }, name, mail, folder);
867 } else {
868 concmdfdg("twit_set name \"%s\" mail \"%s\" folder_mask \"%s\" title \"%s\"", (ConString cmd) { xcmd = cmd.xstrip.idup; }, name, mail, folder, title);
871 try {
872 import std.path : buildPath;
873 auto fo = VFile(buildPath(mailRootDir, "auto_twits.rc"), "a");
874 fo.writeln(xcmd);
875 } catch (Exception e) {
876 conwriteln("ERROR writing twit info: ", e.msg);
879 // HACK: FIXME!
880 auto tw = new TitlerWindow(art.fromname, art.frommail, (acc.inboxFolder.startsWith("dmars_ng/") ? "dmars_ng/*" : acc.inboxFolder), t);
881 tw.onSelected = setdg;
887 })("article_edit_poster_title", "edit poster's title of the current article");
890 // //////////////////////////////////////////////////////////////////// //
891 conRegFunc!(() {
892 if (auto fld = getActiveFolder) {
893 fld.withBaseReader((abase, cur, top, alist) {
894 if (cur < alist.length) {
895 uint ridx = alist[cur];
896 if (auto art = abase[ridx]) {
897 while (abase[ridx].parent != 0) ridx = abase[ridx].parent;
898 twitThread(fld.folderPath, art.msgid);
899 //TODO: mark thread articles as read
903 postScreenRebuild();
905 })("article_twit_thread", "twit current thread");
907 conRegFunc!(() {
908 if (auto fld = getActiveFolder) {
909 if (fld.toggleSoftDeleted()) {
910 if (fld.markAsRead()) {
911 if (mainPane !is null) mainPane.resetUnreadTimer();
913 postScreenRebuild();
916 })("article_softdelete_toggle", "toggle \"soft deleted\" flag on current article");
919 // //////////////////////////////////////////////////////////////////// //
920 conRegFunc!(() {
921 if (auto fld = getActiveFolder) {
922 if (fld.curidxValid) {
923 fld.withBaseReader(delegate (abase, cur, top, alist) {
924 auto art = abase[alist[cur]];
925 if (art !is null) {
926 conwriteln("============================");
927 abase.loadContent(alist[cur]);
928 scope(exit) art.releaseContent();
929 ConString from;
930 foreach (ConString s; art.headersIterator!false) {
931 conwriteln(" ", s);
932 if (from.length == 0 && s.startsWithCI("From:")) from = s;
934 conwriteln("---------------");
935 conwriteln(" ", art.fromname, " <", art.frommail, ">");
936 conwriteln(" ", decodeq(from));
937 conwrite(" ");
938 Utf8DecoderFast dc;
939 foreach (char ch; art.fromname) {
940 if (dc.decode(cast(ubyte)ch)) {
941 if (dc.codepoint > 127 || dc.codepoint < 32) conwritef!" \\u%04X "(cast(uint)dc.codepoint);
942 else conwrite(cast(char)dc.codepoint);
945 conwriteln();
950 })("article_dump_headers", "dump article headers");
952 conRegFunc!(() {
953 if (auto fld = getActiveFolder) {
954 fld.rebuildIndex();
955 postScreenRebuild();
957 })("folder_rebuild_index", "rebuild index file");
959 conRegFunc!(() {
960 if (auto fld = getActiveFolder) {
961 fld.packText();
962 postScreenRebuild();
964 })("folder_pack_textdb", "pack (rebuild) text database for current folder");
966 conRegVar("folder_hide_old", "should old articles be hidden in UI?",
967 delegate (self) {
968 if (auto fld = getActiveFolder) return fld.hideOldThreads;
969 return false;
971 delegate (self, bool nv) {
972 if (auto fld = getActiveFolder) fld.hideOldThreads = nv;
977 conRegFunc!(() {
978 if (auto fld = getActiveFolder) {
979 fld.withBase(delegate (abase) {
980 uint idx = fld.curidx;
981 if (idx >= fld.length) {
982 idx = 0;
983 } else if (auto art = abase[fld.baseidx(idx)]) {
984 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
985 idx = (idx+1)%fld.length;
988 foreach (immutable _; 0..fld.length) {
989 auto art = abase[fld.baseidx(idx)];
990 if (art !is null) {
991 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
992 fld.curidx = cast(int)idx;
993 postScreenRebuild();
994 return;
997 idx = (idx+1)%fld.length;
1001 })("find_mine", "find mine article");
1004 // //////////////////////////////////////////////////////////////////// //
1005 conRegVar!dbg_dump_keynames("dbg_dump_key_names", "dump key names");
1008 // //////////////////////////////////////////////////////////////////// //
1009 conRegFunc!((uint idx, bool runImageViewer=false, ConString fname=null) {
1010 import std.format : format;
1011 if (auto fld = getActiveFolder) {
1012 fld.withBaseReader(delegate (abase, cur, top, alist) {
1013 if (cur >= alist.length) return;
1014 if (auto art = abase[alist[cur]]) {
1015 abase.loadContent(alist[cur]);
1016 scope(exit) art.releaseContent();
1017 string attfname = null;
1018 try {
1019 art.writeAttachmentToFile(idx, delegate (ConString attname) {
1020 //import std.format : format;
1021 import std.file : isDir;
1022 //conwriteln("attnamefix: attname=<", attname, ">; fname=<", fname, ">");
1023 if (fname.length == 0) {
1024 attfname = "/tmp/"~Article.fixAttachmentName(attname);
1025 //conwriteln("attfname(0): <", attfname, ">");
1026 return attfname;
1028 if (fname.isDir) {
1029 string res = fname.idup;
1030 if (res.length && res[$-1] != '/') res ~= '/';
1031 res ~= Article.fixAttachmentName(attname);
1032 attfname = res;
1033 //conwriteln("attfname(1): <", attfname, ">");
1034 return attfname;
1036 attfname = fname.idup;
1037 //conwriteln("attfname(2): <", attfname, ">");
1038 return attfname;
1040 } catch (Exception e) {
1041 conwriteln("ERROR writing attachment (", e.msg, ")");
1042 attfname = null;
1044 if (runImageViewer && attfname.length) {
1045 import std.process;
1046 try {
1047 //FIXME: make regvar for image viewer
1048 //auto pid = execute(["keh", attfname], null, Config.detached);
1049 //pid.wait();
1050 import std.stdio : File;
1051 auto frd = File("/dev/null");
1052 auto fwr = File("/dev/null", "w");
1053 spawnProcess(["keh", attfname], frd, fwr, fwr, null, Config.detached);
1054 } catch (Exception e) {
1055 conwriteln("ERROR executing image viewer (", e.msg, ")");
1061 { import core.memory : GC; GC.collect(); GC.minimize(); }
1062 })("attach_save", "save attach: attach_save index [filename]");
1066 // ////////////////////////////////////////////////////////////////////////// //
1067 //FIXME: turn into property
1068 final class ArticleTextScrollEvent {}
1069 __gshared ArticleTextScrollEvent evArticleScroll;
1070 shared static this () {
1071 evArticleScroll = new ArticleTextScrollEvent();
1074 void postArticleScroll () {
1075 if (vbwin !is null && !vbwin.eventQueued!ArticleTextScrollEvent) vbwin.postTimeout(evArticleScroll, 25);
1079 class MarkAsUnreadEvent {
1080 Folder folder;
1081 uint idx;
1084 __gshared int unreadTimeoutInterval = 600;
1085 __gshared int scrollKeepLines = 3;
1088 void postMarkAsUnreadEvent () {
1089 if (unreadTimeoutInterval > 0 && unreadTimeoutInterval < 10000 && vbwin !is null) {
1090 if (auto fld = getActiveFolder) {
1091 if (fld.curidxValid && fld.curidxUnread) {
1092 //conwriteln("setting new unread timer");
1093 auto evt = new MarkAsUnreadEvent();
1094 evt.folder = fld;
1095 evt.idx = fld.curidx;
1096 vbwin.postTimeout(evt, unreadTimeoutInterval);
1103 __gshared MainPaneWindow mainPane;
1106 final class MainPaneWindow : SubWindow {
1107 string[] emlines; // in utf
1108 uint emlinesBeforeAttaches;
1109 string lastDecodedMsgFolderPath;
1110 string lastDecodedMsgId;
1111 int articleTextTopLine = 0;
1112 int articleDestTextTopLine = 0;
1113 //__gshared Timer unreadTimer; // as main pane is never destroyed, there's no need to kill the timer
1114 //__gshared Folder unreadFolder;
1115 //__gshared uint unreadIdx;
1116 int linesInHeader;
1118 this () {
1119 super(null, 0, 0, VBufWidth, VBufHeight);
1120 mType = Type.OnBottom;
1121 add();
1124 // //////////////////////////////////////////////////////////////////// //
1125 static struct WebLink {
1126 int ly; // in lines
1127 int x, len; // in pixels
1128 string url;
1129 string text; // visual text
1130 int attachnum = -1;
1131 bool nofirst = false;
1133 @property bool isAttach () const pure nothrow @safe @nogc { return (attachnum >= 0); }
1136 WebLink[] emurls;
1137 int lastUrlIndex = -1;
1139 void clearDecodedText () {
1140 emurls[] = WebLink.init;
1141 emlines[] = null;
1142 emurls.length = 0;
1143 emurls.assumeSafeAppend;
1144 emlines.length = 0;
1145 emlines.assumeSafeAppend;
1146 lastDecodedMsgFolderPath = null;
1147 lastDecodedMsgId = null;
1148 articleTextTopLine = 0;
1149 articleDestTextTopLine = 0;
1150 lastUrlIndex = -1;
1153 // <0: not on url
1154 int findUrlIndexAt (int mx, int my) {
1155 int tpX0 = guiGroupListWidth+2+1;
1156 int tpX1 = VBufWidth-1-guiMessageTextLPad*2-guiScrollbarWidth;
1157 int tpY0 = guiThreadListHeight+1;
1158 int tpY1 = VBufHeight-1;
1160 int y = tpY0+linesInHeader*gxTextHeightUtf+2+1+guiMessageTextVPad;
1162 if (mx < tpX0 || mx > tpX1) return -1;
1163 if (my < y || my > tpY1) return -1;
1165 mx -= tpX0;
1167 // yeah, i can easily calculate this, i know
1168 uint idx = articleTextTopLine;
1169 while (idx < emlines.length && y < VBufHeight) {
1170 if (my >= y && my < y+gxTextHeightUtf) {
1171 foreach (immutable uidx, const ref WebLink wl; emurls) {
1172 //conwriteln("checking url [#", uidx, "]; idx=", idx, "; ly=", wl.ly);
1173 if (wl.ly == idx) {
1174 if (mx >= wl.x && mx < wl.x+wl.len) return cast(int)uidx;
1178 ++idx;
1179 y += gxTextHeightUtf+guiMessageTextInterline;
1182 return -1;
1185 WebLink* findUrlAt (int mx, int my) {
1186 auto uidx = findUrlIndexAt(mx, my);
1187 return (uidx >= 0 ? &emurls[uidx] : null);
1190 private void emlDetectUrls (uint textlines) {
1191 static immutable string[3] protos = [ "https", "http", "ftp" ];
1193 lastUrlIndex = -1;
1195 static sptrdiff urlepos (const(char)[] s, sptrdiff spos) {
1196 assert(spos < s.length);
1197 spos = s.indexOf("://", spos);
1198 assert(spos >= 0);
1199 spos += 3;
1200 // host
1201 while (spos < s.length) {
1202 char ch = s[spos];
1203 if (ch == '/') break;
1204 if (ch <= ' ') return spos;
1205 if (ch != '.' && ch != '-' && !ch.isalnum) return spos;
1206 ++spos;
1208 if (spos >= s.length) return cast(sptrdiff)s.length;
1209 // path
1210 assert(s[spos] == '/');
1211 char[16] brcmap;
1212 usize brlevel = 0;
1213 bool wasSharp = false;
1214 for (; spos < s.length; ++spos) {
1215 import iv.strex : isalnum;
1216 char ch = s[spos];
1217 if (ch <= ' ' || ch == '<' || ch == '>' || ch == '"' || ch == '\'' || ch >= 127) return spos;
1218 // hash
1219 if (ch == '#') {
1220 if (wasSharp) return spos;
1221 wasSharp = true;
1222 brlevel = 0;
1223 continue;
1225 // path delimiter
1226 if (ch == '/') {
1227 brlevel = 0;
1228 continue;
1230 // opening bracket
1231 if (ch == '(' || ch == '{' || ch == '[') {
1232 if (brlevel >= brcmap.length) return spos; // too nested
1233 if (s.length-spos < 2) return spos; // no more chars, ignore
1234 if (!isalnum(s[spos+1]) && s[spos+1] != '_') return spos; // ignore
1235 // looks like URL part
1236 final switch (ch) {
1237 case '(': ch = ')'; break;
1238 case '[': ch = ']'; break;
1239 case '{': ch = '}'; break;
1241 brcmap[brlevel++] = ch;
1242 continue;
1244 // closing bracket
1245 if (ch == ')' || ch == '}' || ch == ']') {
1246 if (brlevel == 0 || ch != brcmap[brlevel-1]) return spos;
1247 --brlevel;
1248 continue;
1250 // other punctuation
1251 if (brlevel == 0 && !isalnum(ch)) {
1252 // other special chars
1253 if (s.length-spos < 2) return spos; // no more chars, ignore
1254 if (!isalnum(s[spos+1]) && s[spos+1] != '_') {
1255 if (ch == '.' || ch == '!' || ch == ';' || ch == ',' || s[spos+1] != ch) return spos; // ignore
1258 ++spos;
1260 if (spos >= s.length) spos = cast(sptrdiff)s.length;
1261 return spos;
1264 if (textlines > emlines.length) textlines = cast(uint)emlines.length; // just in case
1265 foreach (immutable cy, string s; emlines[0..textlines]) {
1266 if (s.length == 0) continue;
1267 auto pos = s.indexOf("://");
1268 while (pos > 0) {
1269 bool found = false;
1270 auto spos = pos;
1271 foreach (string proto; protos) {
1272 if (spos >= proto.length && proto.strEquCI(s[spos-proto.length..spos])) {
1273 if (spos == proto.length || !s[spos-proto.length-1].isalnum) {
1274 found = true;
1275 spos -= proto.length;
1276 break;
1280 if (found) {
1281 // find URL end
1282 auto epos = urlepos(s, spos);
1283 WebLink wl;
1284 wl.nofirst = (spos > 0);
1285 wl.ly = cast(int)cy;
1286 auto kr = GxKerning(4);
1287 s[0..epos].utfByDCharSPos(delegate (dchar ch, usize stpos) {
1288 int w = kr.fixWidthPre(ch);
1289 if (stpos == spos) wl.x = w;
1291 wl.len = kr.finalWidth-wl.x;
1292 wl.url = wl.text = s[spos..epos];
1293 emurls ~= wl;
1294 pos = epos;
1295 } else {
1296 ++pos;
1298 pos = s.indexOf("://", pos);
1302 int attachcount = 0;
1303 foreach (immutable uint cy; textlines..cast(uint)emlines.length) {
1304 string s = emlines[cy];
1305 if (s.length == 0) continue;
1306 auto spos = s.indexOf("attach:");
1307 if (spos < 0) continue;
1308 auto epos = spos+7;
1309 while (epos < s.length && s.ptr[epos] > ' ') ++epos;
1310 //if (attachcount >= parts.length) break;
1311 WebLink wl;
1312 wl.nofirst = (spos > 0);
1313 wl.ly = cast(int)cy;
1314 //wl.x = gxTextWidthUtf(s[0..spos]);
1315 //wl.len = gxTextWidthUtf(s[spos..epos]);
1316 auto kr = GxKerning(4);
1317 s[0..epos].utfByDCharSPos(delegate (dchar ch, usize stpos) {
1318 int w = kr.fixWidthPre(ch);
1319 if (stpos == spos) wl.x = w;
1321 wl.len = kr.finalWidth-wl.x;
1322 wl.url = wl.text = s[spos..epos];
1323 //if (spos > 0) ++wl.x; // this makes text bolder, no need to
1324 wl.attachnum = attachcount;
1325 //wl.attachfname = s[spos+7..epos];
1326 //wl.part = parts[attachcount];
1327 ++attachcount;
1328 emurls ~= wl;
1332 bool needToDecodeArticleTextNL (Folder fld, Article art) nothrow @trusted @nogc {
1333 if (art is null) return false;
1334 if (lastDecodedMsgId != art.msgid) return true;
1335 return (fld !is null ? lastDecodedMsgFolderPath != fld.folderPath : lastDecodedMsgFolderPath.length != 0);
1338 // fld is locked here
1339 void decodeArticleTextNL (Folder fld, Article art) {
1340 if (art is null) { clearDecodedText(); return; }
1341 if (!needToDecodeArticleTextNL(fld, art)) return;
1343 clearDecodedText();
1344 emlines ~= null; // hack; this dummy line will be removed
1345 lastDecodedMsgFolderPath = (fld !is null ? fld.folderPath : null);
1346 lastDecodedMsgId = art.msgid;
1348 bool lastEndsWithSpace () { return (emlines[$-1].length ? emlines[$-1][$-1] == ' ' : false); }
1350 int lastQLevel = 0;
1352 static string addQuotes(T:const(char)[]) (T s, int qlevel) {
1353 enum QuoteStr = ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ";
1354 if (qlevel <= 0) {
1355 static if (is(T == string) || is (T == typeof(null))) return s; else return s.idup;
1357 if (qlevel > QuoteStr.length-1) qlevel = cast(int)QuoteStr.length-1;
1358 return QuoteStr[$-qlevel-1..$]~s;
1361 // returns quote level
1362 static int removeQuoting (ref ConString s) {
1363 // calculate quote level
1364 int qlevel = 0;
1365 if (s.length && s[0] == '>') {
1366 usize lastqpos = 0, pos = 0;
1367 while (pos < s.length) {
1368 if (s.ptr[pos] != '>') {
1369 if (s.ptr[pos] != ' ') break;
1370 } else {
1371 lastqpos = pos;
1372 ++qlevel;
1374 ++pos;
1376 if (s.length-lastqpos > 1 && s.ptr[lastqpos+1] == ' ') ++lastqpos;
1377 ++lastqpos;
1378 s = s[lastqpos..$];
1380 return qlevel;
1383 bool inCode = false;
1385 void putLine (ConString s) {
1386 int qlevel = removeQuoting(s);
1387 //conwriteln("qlevel=", qlevel, "; s=[", s, "]");
1388 // empty line: just insert it
1389 if (s.length == 0) {
1390 emlines ~= addQuotes(null, qlevel).xstripright;
1391 } else {
1392 // join code lines if it is possible
1393 if (inCode && qlevel == lastQLevel && lastEndsWithSpace) {
1394 //conwriteln("<", s, ">");
1395 emlines[$-1] ~= addQuotes(s.xstrip, qlevel);
1396 return;
1398 // two spaces at the beginning usually means "this is code"; don't wrap it
1399 if (s.length >= 1 && s[0] == '\t') {
1400 emlines ~= addQuotes(s, qlevel);
1401 // join next lines if it is possible
1402 inCode = true;
1403 //conwriteln("[", s, "]");
1404 lastQLevel = qlevel;
1405 return;
1407 inCode = false;
1408 // can we append?
1409 bool newline = false;
1410 if (lastQLevel != qlevel || !lastEndsWithSpace) {
1411 newline = true;
1412 } else {
1413 // append words
1414 //while (s.length > 1 && s.ptr[0] <= ' ') s = s[1..$];
1416 while (s.length) {
1417 usize epos = 0;
1418 if (newline) {
1419 while (epos < s.length && s.ptr[epos] <= ' ') ++epos;
1420 } else {
1421 //assert(s[0] > ' ');
1422 while (epos < s.length && s.ptr[epos] <= ' ') ++epos;
1424 while (epos < s.length && s.ptr[epos] > ' ') ++epos;
1425 auto xlen = epos;
1426 while (epos < s.length && s.ptr[epos] <= ' ') ++epos;
1427 if (!newline && emlines[$-1].length+xlen <= 80) {
1428 // no wrapping, continue last line
1429 emlines[$-1] ~= s[0..epos];
1430 } else {
1431 newline = false;
1432 // wrapping; new line
1433 emlines ~= addQuotes(s[0..epos], qlevel);
1435 s = s[epos..$];
1437 if (newline) emlines ~= addQuotes(null, qlevel);
1439 lastQLevel = qlevel;
1442 try {
1443 foreach (ConString s; LineIterator!false(art.getTextContent)) putLine(s);
1444 // remove first dummy line
1445 if (emlines.length) emlines = emlines[1..$];
1446 // remove trailing empty lines
1447 while (emlines.length && emlines[$-1].xstrip.length == 0) emlines.length -= 1;
1448 } catch (Exception e) {
1449 conwriteln("================================= ERROR: ", e.msg, " =================================");
1450 conwriteln(e.toString);
1453 // attaches
1454 auto lcount = cast(uint)emlines.length;
1455 emlinesBeforeAttaches = lcount;
1457 uint attcount = 0;
1458 art.forEachAttachment(delegate(ConString type, ConString filename) {
1459 if (attcount == 0) { emlines ~= null; emlines ~= null; }
1460 import std.format : format;
1461 if (type.length == 0) type = "unknown/unknown";
1462 string s = " [%02s] attach:%s -- %s".format(attcount, Article.fixAttachmentName(filename), type);
1463 emlines ~= s;
1464 ++attcount;
1465 return false;
1467 emlDetectUrls(lcount);
1470 @property int visibleArticleLines () {
1471 int y = guiThreadListHeight+1+linesInHeader*gxTextHeightUtf+2+guiMessageTextVPad;
1472 return (VBufHeight-y)/(gxTextHeightUtf+guiMessageTextInterline);
1475 void normalizeArticleTopLine () {
1476 int lines = visibleArticleLines;
1477 if (lines < 1 || emlines.length <= lines) {
1478 articleTextTopLine = 0;
1479 articleDestTextTopLine = 0;
1480 } else {
1481 if (articleTextTopLine < 0) articleTextTopLine = 0;
1482 if (articleTextTopLine+lines > emlines.length) {
1483 articleTextTopLine = cast(int)emlines.length-lines;
1484 if (articleTextTopLine < 0) articleTextTopLine = 0;
1489 void doScrollStep () {
1490 auto oldtop = articleTextTopLine;
1491 foreach (immutable _; 0..6) {
1492 normalizeArticleTopLine();
1493 if (articleDestTextTopLine < articleTextTopLine) {
1494 --articleTextTopLine;
1495 } else if (articleDestTextTopLine > articleTextTopLine) {
1496 ++articleTextTopLine;
1497 } else {
1498 break;
1500 normalizeArticleTopLine();
1502 if (articleTextTopLine == oldtop) {
1503 // can't scroll anymore
1504 articleDestTextTopLine = articleTextTopLine;
1505 return;
1507 postScreenRebuild();
1508 postArticleScroll();
1511 void scrollBy (int delta) {
1512 articleDestTextTopLine += delta;
1513 doScrollStep();
1516 void scrollByPageUp () {
1517 int lines = visibleArticleLines-scrollKeepLines;
1518 if (lines < 1) lines = 1;
1519 scrollBy(-lines);
1522 void scrollByPageDown () {
1523 int lines = visibleArticleLines-scrollKeepLines;
1524 if (lines < 1) lines = 1;
1525 scrollBy(lines);
1528 void resetUnreadTimer () {
1530 if (unreadTimer !is null) { unreadTimer.destroy(); unreadTimer = null; }
1531 unreadFolder = null;
1532 unreadIdx = unreadIdx.max;
1536 void setupUnreadTimer () {
1537 postMarkAsUnreadEvent();
1539 if (auto fld = getActiveFolder) {
1540 if (fld.curidxValid && fld.curidxUnread) {
1541 conwriteln("setting new unread timer");
1542 resetUnreadTimer();
1543 unreadFolder = fld;
1544 unreadIdx = fld.curidx;
1545 unreadTimer = new Timer(2000, delegate () {
1546 conwriteln("unread timer fired");
1547 if (unreadFolder is getActiveFolder && unreadIdx == unreadFolder.curidx) {
1548 conwriteln("*** unread timer hit!");
1549 unreadFolder.curidxUnread = false;
1551 resetUnreadTimer();
1553 //fld.curidxUnread = false;
1556 resetUnreadTimer();
1560 //enum gxRGBA = (clampToByte(a)<<24)|(clampToByte(r)<<16)|(clampToByte(g)<<8)|clampToByte(b);
1561 uint groupListBackColor = gxRGB!(30, 30, 30);
1562 uint groupListDivLineColor = gxRGB!(255, 255, 255);
1564 uint threadListBackColor = gxRGB!(30, 30, 30);
1565 uint threadListDivLineColor = gxRGB!(255, 255, 255);
1567 uint messageHeaderBackColor = gxRGB!(20, 20, 20);
1568 uint messageHeaderFromColor = gxRGB!(0, 128, 128);
1569 uint messageHeaderToColor = gxRGB!(0, 128, 128);
1570 uint messageHeaderSubjColor = gxRGB!(0, 128, 128);
1571 uint messageHeaderDateColor = gxRGB!(0, 128, 128);
1572 uint messageHeaderDivLineColor = gxRGB!(180, 180, 180);
1574 uint messageTextBackColor = gxRGB!(37, 37, 37);
1575 uint messageTextNormalColor = gxRGB!(128+46, 128+46, 128+46);
1576 uint messageTextQuote0Color = gxRGB!(128, 128, 0);
1577 uint messageTextQuote1Color = gxRGB!(0, 128, 128);
1578 uint messageTextLinkColor = gxRGB!(0, 200, 200);
1579 uint messageTextLinkHoverColor = gxRGB!(0, 255, 255);
1580 uint messageTextLinkPressedColor = gxRGB!(255, 0, 255);
1582 uint twithShadeColor = gxRGBA!(0, 0, 80, 127);
1583 uint twithTextColor = gxRGB!(255, 0, 0);
1584 uint twithTextOutlineColor = gxRGB!(0, 0, 0);
1586 // //////////////////////////////////////////////////////////////////// //
1587 //TODO: move parts to widgets
1588 override void onPaint () {
1589 gxClipReset();
1591 gxFillRect(0, 0, guiGroupListWidth, VBufHeight, groupListBackColor);
1592 gxVLine(guiGroupListWidth, 0, VBufHeight, groupListDivLineColor);
1594 gxFillRect(guiGroupListWidth+1, 0, VBufWidth, guiThreadListHeight, threadListBackColor);
1595 gxHLine(guiGroupListWidth+1, guiThreadListHeight, VBufWidth, threadListDivLineColor);
1597 // called with locked folder
1598 void drawArticle (ArticleBase abase, Folder fld, Article art, uint cidx, uint aidx) {
1599 import core.stdc.stdio : snprintf;
1600 import std.format : format;
1601 import std.datetime;
1602 char[128] tbuf;
1603 const(char)[] tbufs;
1605 void xfmt (string s, const(char)[][] strs...) {
1606 int dpos = 0;
1607 void puts (const(char)[] s...) {
1608 foreach (char ch; s) {
1609 if (dpos >= tbuf.length) break;
1610 tbuf[dpos++] = ch;
1613 while (s.length) {
1614 if (strs.length && s.length > 1 && s[0] == '%' && s[1] == 's') {
1615 puts(strs[0]);
1616 strs = strs[1..$];
1617 s = s[2..$];
1618 } else {
1619 puts(s[0]);
1620 s = s[1..$];
1623 tbufs = tbuf[0..dpos];
1626 if (needToDecodeArticleTextNL(fld, art)) {
1627 abase.loadContent(aidx);
1628 scope(exit) abase.releaseContent(aidx);
1629 assert(art.contentLoaded);
1630 decodeArticleTextNL(fld, art);
1634 gxClipX0 = guiGroupListWidth+2;
1635 gxClipX1 = VBufWidth-1;
1636 gxClipY0 = guiThreadListHeight+1;
1637 gxClipY1 = VBufHeight-1;
1639 gxClipRect = GxRect(GxPoint(guiGroupListWidth+2, guiThreadListHeight+1), GxPoint(VBufWidth-1, VBufHeight-1));
1641 int msx = lastMouseX;
1642 int msy = lastMouseY;
1644 int curDrawYMul = 1;
1646 // header
1647 immutable int hdrHeight = (3+(art.toname.length || art.tomail.length ? 1 : 0))*gxTextHeightUtf+2;
1648 gxFillRect(gxClipRect.x0, gxClipRect.y0, gxClipRect.x1-gxClipRect.x0+1, hdrHeight, messageHeaderBackColor);
1650 xfmt("From: %s <%s>", art.fromname, art.frommail);
1651 gxDrawTextUtf(gxClipRect.x0+guiMessageTextLPad, gxClipRect.y0+0*gxTextHeightUtf+1, tbufs, messageHeaderFromColor);
1652 if (art.toname.length || art.tomail.length) {
1653 xfmt("To: %s <%s>", art.toname, art.tomail);
1654 gxDrawTextUtf(gxClipRect.x0+guiMessageTextLPad, gxClipRect.y0+curDrawYMul*gxTextHeightUtf+1, tbufs, messageHeaderToColor);
1655 ++curDrawYMul;
1657 xfmt("Subject: %s", art.subj);
1658 gxDrawTextUtf(gxClipRect.x0+guiMessageTextLPad, gxClipRect.y0+curDrawYMul*gxTextHeightUtf+1, tbufs, messageHeaderSubjColor);
1659 ++curDrawYMul;
1661 auto t = SysTime.fromUnixTime(art.time);
1662 //string s = "Date: %04d/%02d/%02d %02d:%02d:%02d".format(t.year, t.month, t.day, t.hour, t.minute, t.second);
1663 auto tlen = snprintf(tbuf.ptr, tbuf.length, "Date: %04d/%02d/%02d %02d:%02d:%02d", t.year, t.month, t.day, t.hour, t.minute, t.second);
1664 gxDrawTextUtf(gxClipRect.x0+guiMessageTextLPad, gxClipRect.y0+curDrawYMul*gxTextHeightUtf+1, tbuf[0..tlen], messageHeaderDateColor);
1665 ++curDrawYMul;
1669 // text
1670 linesInHeader = curDrawYMul;
1671 int y = gxClipRect.y0+curDrawYMul*gxTextHeightUtf+2;
1673 gxHLine(gxClipRect.x0, y, gxClipRect.x1-gxClipRect.x0+1, messageHeaderDivLineColor);
1674 ++y;
1675 gxFillRect(gxClipRect.x0, y, gxClipRect.x1-gxClipRect.x0+1, VBufHeight-y, messageTextBackColor);
1676 y += guiMessageTextVPad;
1678 immutable sty = y;
1680 normalizeArticleTopLine();
1682 bool drawUpMark = (articleTextTopLine > 0);
1683 bool drawDownMark = false;
1685 uint idx = articleTextTopLine;
1686 bool msvisible = isMouseVisible;
1687 while (idx < emlines.length && y < VBufHeight) {
1688 int qlevel = 0;
1689 string s = emlines[idx];
1691 foreach (char ch; s) {
1692 if (ch <= ' ') continue;
1693 if (ch != '>') break;
1694 ++qlevel;
1697 uint clr = messageTextNormalColor;
1698 if (qlevel) {
1699 final switch (qlevel%2) {
1700 case 0: clr = messageTextQuote0Color; break;
1701 case 1: clr = messageTextQuote1Color; break;
1705 gxDrawTextUtf(GxDrawTextOptions.TabColor(4, clr), gxClipRect.x0+guiMessageTextLPad, y, s);
1707 foreach (const ref WebLink wl; emurls) {
1708 if (wl.ly == idx) {
1709 uint lclr = messageTextLinkColor;
1710 if (msvisible && msy >= y && msy < y+gxTextHeightUtf &&
1711 msx >= gxClipRect.x0+1+guiMessageTextLPad+wl.x &&
1712 msx < gxClipRect.x0+1+guiMessageTextLPad+wl.x+wl.len)
1714 lclr = (lastMouseLeft ? messageTextLinkPressedColor : messageTextLinkHoverColor);
1716 gxDrawTextUtf(GxDrawTextOptions.TabColorFirstFull(4, lclr, wl.nofirst), gxClipRect.x0+guiMessageTextLPad+wl.x, y, wl.text);
1720 if (gxClipRect.y1-y < gxTextHeightUtf && emlines.length-idx > 0) drawDownMark = true;
1722 ++idx;
1723 y += gxTextHeightUtf+guiMessageTextInterline;
1726 //gxDrawTextOutScaledUtf(1, gxClipRect.x1-gxTextWidthUtf(triangleDownStr)-3, sty, triangleDownStr, (drawUpMark ? gxRGB!(255, 255, 255) : gxRGB!(85, 85, 85)), gxRGB!(0, 69, 69));
1727 //gxDrawTextOutScaledUtf(1, gxClipRect.x1-gxTextWidthUtf(triangleUpStr)-3, gxClipRect.y1-7, triangleUpStr, (drawDownMark ? gxRGB!(255, 255, 255) : gxRGB!(85, 85, 85)), gxRGB!(0, 69, 69));
1729 gxDrawScrollBar(GxRect(gxClipRect.x1-10, sty+10, 5, gxClipRect.y1-sty-17), cast(int)emlines.length-1, idx-1);
1731 string twittext = fld.isTwittedNL(cidx);
1732 if (twittext !is null) {
1733 foreach (immutable dy; gxClipRect.y0+3*gxTextHeightUtf+2..gxClipRect.y1+1) {
1734 foreach (immutable dx; gxClipRect.x0..gxClipRect.x1+1) {
1735 if ((dx^dy)&1) gxPutPixel(dx, dy, twithShadeColor);
1739 if (twittext.length) {
1740 int tx = gxClipRect.x0+(gxClipRect.width-gxTextWidthScaledUtf(2, twittext))/2-1;
1741 int ty = gxClipRect.y0+(gxClipRect.height-3*gxTextHeightUtf)/2-1;
1742 gxDrawTextOutScaledUtf(2, tx, ty, twittext, twithTextColor, twithTextOutlineColor);
1747 void drawThreadList (Folder fld) {
1748 if (fld.needRebuild) fld.buildVisibleList();
1749 fld.makeCurrentVisible();
1750 fld.withBaseReader(delegate (abase, cur, top, alist) {
1751 if (alist.length == 0) return;
1753 gxClipRect.x0 = guiGroupListWidth+2;
1754 gxClipRect.x1 = VBufWidth-1-5;
1755 gxClipRect.y0 = 0;
1756 gxClipRect.y1 = guiThreadListHeight-1;
1757 immutable uint origX0 = gxClipRect.x0;
1758 immutable uint origX1 = gxClipRect.x1;
1759 immutable uint origY0 = gxClipRect.y0;
1760 immutable uint origY1 = gxClipRect.y1;
1761 int y = 0;
1762 //conwriteln(fld.msgtop, " : ", fld.list.length);
1763 uint idx = top;
1764 while (idx < alist.length && y < guiThreadListHeight) {
1765 import std.format : format;
1766 import std.datetime;
1768 if (y >= guiThreadListHeight) break;
1769 if (idx >= alist.length) break;
1771 gxClipRect.x0 = origX0;
1772 gxClipRect.x1 = origX1;
1774 auto art = abase[alist[idx]];
1776 //conwriteln(idx, " : ", fld.list.length);
1777 if (idx == cur) {
1778 uint cc = gxRGB!(0, 127, 127);
1779 if (art.softDeleted) cc = gxRGB!(0, 127-30, 127-30);
1780 gxFillRect(gxClipRect.x0, y, gxClipRect.width-1, gxTextHeightUtf, cc);
1782 gxClipRect.x0 = gxClipRect.x0+1;
1783 gxClipRect.x1 = gxClipRect.x1-1;
1785 //uint clr = (idx != fld.curidx ? gxRGB!(255, 127, 0) : gxRGB!(255, 255, 255));
1786 uint clr = (art.unread ? gxRGB!(255, 255, 255) : gxRGB!(255-60, 127-60, 0));
1787 uint clr1 = (art.unread ? gxRGB!(255, 255, 0) : gxRGB!(255-60-40, 127-60-40, 0));
1789 if (art.depth != 0 && !art.unread) {
1790 clr = gxRGB!(255-90, 127-90, 0);
1791 clr1 = gxRGB!(255-90-40, 127-90-40, 0);
1794 if (!art.unread && !art.softDeleted) {
1795 if (fld.isTwittedNL(idx) !is null) { clr = gxRGB!(60, 0, 0); clr1 = gxRGB!(60, 0, 0); }
1796 if (fld.isHighlightedNL(idx)) { clr = gxRGB!(0, 190, 0); clr1 = gxRGB!(0, 190-40, 0); }
1797 } else if (art.softDeleted) {
1798 //clr = gxRGB!(255-80, 127-80, 0);
1799 //clr1 = gxRGB!(255-80-40, 127-80-40, 0);
1800 clr = clr1 = gxRGB!(127, 0, 0);
1803 auto t = SysTime.fromUnixTime(art.time);
1804 //string s = "%04d/%02d/%02d %02d:%02d".format(t.year, t.month, t.day, t.hour, t.minute);
1805 int timewdt;
1807 import core.stdc.stdio : snprintf;
1808 char[128] tmpbuf;
1809 //string s = "%02d/%02d %02d:%02d".format(t.month, t.day, t.hour, t.minute);
1810 auto len = snprintf(tmpbuf.ptr, tmpbuf.length, "%02d/%02d/%02d %02d:%02d", t.year%100, t.month, t.day, t.hour, t.minute);
1811 timewdt = gxDrawTextUtf(gxClipRect.x1-gxTextWidthUtf(tmpbuf[0..len]), y, tmpbuf[0..len], clr);
1813 if (timewdt%8) timewdt = (timewdt|7)+1;
1815 string from = art.fromname;
1817 auto vp = from.indexOf(" via Digitalmars-");
1818 if (vp > 0) {
1819 from = from[0..vp].xstrip;
1820 if (from.length == 0) from = "anonymous";
1824 gxClipRect.x1 = gxClipRect.x1-/*(13*6+4)*2+33*/timewdt;
1825 enum FromWidth = 22*6*2+88;
1826 gxDrawTextUtf(gxClipRect.x1-FromWidth, y, from, clr);
1827 gxDrawTextUtf(gxClipRect.x1-FromWidth+gxTextWidthUtf(from)+4, y, "<", clr1);
1828 gxDrawTextUtf(gxClipRect.x1-FromWidth+gxTextWidthUtf(from)+4+gxTextWidthUtf("<")+1, y, art.frommail, clr1);
1829 gxDrawTextUtf(gxClipRect.x1-FromWidth+gxTextWidthUtf(from)+4+gxTextWidthUtf("<")+1+gxTextWidthUtf(art.frommail)+1, y, ">", clr1);
1831 gxClipRect.x1 = gxClipRect.x1-FromWidth;
1832 gxDrawTextUtf(gxClipRect.x0+art.depth*3, y, art.subj, clr);
1833 foreach (immutable dx; 0..art.depth) gxPutPixel(gxClipRect.x0+1+dx*3, y+gxTextHeightUtf/2, gxRGB!(70, 70, 70));
1835 if (art.softDeleted) {
1836 gxClipRect.x0 = origX0;
1837 gxClipRect.x1 = origX1;
1838 gxHLine(gxClipRect.x0, y+gxTextHeightUtf/2, gxClipRect.x1-gxClipRect.x0+1, clr);
1841 ++idx;
1842 y += gxTextHeightUtf;
1845 // draw progressbar
1847 gxClipRect.x0 = origX0;
1848 gxClipRect.x1 = origX1+5;
1849 gxClipRect.y0 = origY0;
1850 gxClipRect.y1 = origY1;
1851 gxDrawScrollBar(GxRect(gxClipRect.x1-5, gxClipRect.y0, 4, gxClipRect.height-1), cast(int)alist.length-1, idx-1);
1854 if (cur < alist.length) drawArticle(abase, fld, abase[alist[cur]], cur, alist[cur]);
1859 folderMakeCurVisible();
1860 int ofsx = 2;
1861 int ofsy = 1;
1862 foreach (immutable idx, Folder fld; folders) {
1863 if (idx < folderTop) continue;
1864 if (ofsy >= VBufHeight) break;
1865 gxClipReset();
1866 if (idx == folderCur) gxFillRect(0, ofsy-1, guiGroupListWidth, (gxTextHeightUtf+2), gxRGB!(0, 127, 127));
1867 gxClipRect.x0 = ofsx-1;
1868 gxClipRect.y0 = ofsy;
1869 gxClipRect.x1 = guiGroupListWidth-3;
1870 int depth = folderDepth(idx);
1871 uint clr = gxRGB!(255-30, 127-30, 0);
1872 if (fld.unreadCount) {
1873 clr = gxRGB!(0, 255, 255);
1874 } else {
1875 if (depth == 0) {
1876 clr = (fld.folderPath == "accounts" ? gxRGB!(220, 220, 0) : gxRGB!(255, 127+60, 0));
1877 } else if (fld.folderPath.startsWith("accounts/") && fld.folderPath.endsWith("/inbox")) {
1878 clr = gxRGB!(255, 127+30, 0);
1881 foreach (immutable dd; 0..depth) gxPutPixel(ofsx+dd*6+2, ofsy+gxTextHeightUtf/2, gxRGB!(80, 80, 80));
1882 gxDrawTextOutScaledUtf(1, ofsx+depth*6, ofsy, folderVisName(idx), clr, gxRGB!(0, 0, 0));
1883 ofsy += gxTextHeightUtf+2;
1887 if (folderCur < folders.length) drawThreadList(folders[folderCur]);
1890 override bool onKey (KeyEvent event) {
1891 if (event.pressed) {
1892 if (dbg_dump_keynames) conwriteln("key: ", event.toStr, ": ", event.modifierState&ModifierState.windows);
1893 //foreach (const ref kv; mainAppKeyBindings.byKeyValue) if (event == kv.key) concmd(kv.value);
1894 char[64] kname;
1895 if (auto cmdp = event.toStrBuf(kname[]) in mainAppKeyBindings) {
1896 concmd(*cmdp);
1897 return true;
1899 // debug
1901 if (event == "S-Up") {
1902 if (folderTop > 0) --folderTop;
1903 postScreenRebuild();
1904 return true;
1906 if (event == "S-Down") {
1907 if (folderTop+1 < folders.length) ++folderTop;
1908 postScreenRebuild();
1909 return true;
1912 //if (event == "Tab") { new PostWindow(); return true; }
1913 //if (event == "Tab") { new SelectPopBoxWindow(null); return true; }
1915 return false;
1918 // returning `false` to avoid screen rebuilding by dispatcher
1919 override bool onMouse (MouseEvent event) {
1920 //FIXME: use window coordinates
1921 int mx, my;
1922 event.mouse2xy(mx, my);
1923 // button press
1924 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
1925 // select folder
1926 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < VBufHeight) {
1927 uint fnum = my/(gxTextHeightUtf+2)+folderTop;
1928 if (fnum != folderCur) {
1929 folderCur = fnum;
1930 postScreenRebuild();
1932 return false;
1934 // select post
1935 if (mx > guiGroupListWidth && mx < VBufWidth && my >= 0 && my < guiThreadListHeight) {
1936 if (auto fld = getActiveFolder) {
1937 my /= gxTextHeightUtf;
1938 if (fld.curidx != fld.msgtop+my) {
1939 fld.curidx = fld.msgtop+my;
1940 if (fld.curidxValid && fld.curidxUnread) fld.curidxUnread = false;
1941 postScreenRebuild();
1943 return false;
1947 // wheel
1948 if (event.type == MouseEventType.buttonPressed && (event.button == MouseButton.wheelUp || event.button == MouseButton.wheelDown)) {
1949 // folder
1950 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < VBufHeight) {
1951 if (event.button == MouseButton.wheelUp) {
1952 if (folderCur > 0) folderCur = folderCur-1;
1953 } else {
1954 if (folderCur+1 < folders.length) folderCur = folderCur+1;
1956 postScreenRebuild();
1957 return false;
1959 // post
1960 if (mx > guiGroupListWidth && mx < VBufWidth && my >= 0 && my < guiThreadListHeight) {
1961 if (auto fld = getActiveFolder) {
1962 if (event.button == MouseButton.wheelUp) fld.moveUp(); else fld.moveDown();
1963 postScreenRebuild();
1964 return false;
1967 // text
1968 if (mx > guiGroupListWidth && mx < VBufWidth && my > guiThreadListHeight && my < VBufHeight) {
1969 enum ScrollLines = 2;
1970 if (event.button == MouseButton.wheelUp) scrollBy(-ScrollLines); else scrollBy(ScrollLines);
1971 postScreenRebuild();
1972 return false;
1975 // button release
1976 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
1977 // try url
1978 auto url = findUrlAt(mx, my);
1979 if (url !is null) {
1980 if (url.isAttach) {
1981 concmdf!"attach_save %s %s"(url.attachnum, ((event.modifierState&ModifierState.alt) != 0));
1982 } else {
1983 if (event.modifierState&(ModifierState.shift|ModifierState.ctrl)) {
1984 //conwriteln("link-to-clipboard: <", url.url, ">");
1985 setClipboardText(vbwin, url.url); // it is safe to cast here
1986 setPrimarySelection(vbwin, url.url); // it is safe to cast here
1987 conwriteln("url copied to the clipboard.");
1988 } else {
1989 //conwriteln("link-open: <", url.url, ">");
1990 concmdf!"open_url \"%s\" %s"(url.url, ((event.modifierState&ModifierState.alt) != 0));
1995 if (event.type == MouseEventType.motion) {
1996 auto uidx = findUrlIndexAt(mx, my);
1997 if (uidx != lastUrlIndex) { lastUrlIndex = uidx; postScreenRebuild(); return false; }
2000 if (event.type == MouseEventType.buttonPressed || event.type == MouseEventType.buttonReleased) {
2001 postScreenRebuild();
2002 } else {
2003 postScreenRepaint();
2006 return false;
2009 override bool onChar (dchar ch) {
2010 return false;
2015 // ////////////////////////////////////////////////////////////////////////// //
2016 void rebuildScreen (SimpleWindow w) {
2017 if (w !is null && !w.closed && !w.hidden) {
2018 static if (EGfxOpenGLBackend) {
2019 w.redrawOpenGlSceneNow();
2020 } else {
2021 bool resizeWin = false;
2023 consoleLock();
2024 scope(exit) consoleUnlock();
2025 if (!conQueueEmpty()) postDoConCommands();
2026 if (vbufNewScale != vbufEffScale) {
2027 // window scale changed
2028 resizeWin = true;
2030 if (vbufEffVSync != vbufVSync) {
2031 vbufEffVSync = vbufVSync;
2034 if (resizeWin) {
2035 w.resize(winWidthScaled, winHeightScaled);
2036 glconResize(winWidthScaled, winHeightScaled);
2037 //vglResizeBuffer(VBufWidth, VBufHeight, vbufNewScale);
2039 /*if (egx11img !is null)*/ {
2040 //glconBackBuffer = egx11img;
2041 gxClipReset();
2042 gxClearScreen(0);
2043 paintSubWindows();
2044 vglUpdateTexture();
2045 //vglBlitTexture();
2046 if (vArrowTextureId) {
2047 if (isMouseVisible) {
2048 int px = lastMouseX;
2049 int py = lastMouseY;
2050 //conwriteln("msalpha: ", mouseAlpha);
2051 //glColor4f(1, 1, 1, mouseAlpha);
2052 vglBlitArrow(px, py);
2055 //glconDraw();
2057 auto painter = w.draw();
2058 vglBlitTexture(w);
2059 glconDrawWindow = w;
2060 glconDrawDirect = false;
2061 glconDraw();
2062 glconDrawWindow = null;
2063 //painter.drawImage(Point(0, 0), egx11img);
2066 if (isQuitRequested()) w.postEvent(new QuitEvent());
2068 flushGui();
2073 void repaintScreen (SimpleWindow w) {
2074 if (w !is null && !w.closed && !w.hidden) {
2075 static if (EGfxOpenGLBackend) {
2076 w.redrawOpenGlSceneNow();
2077 flushGui();
2078 } else {
2079 static bool lastvisible = false;
2080 bool curvisible = isConsoleVisible;
2081 if (lastvisible != curvisible || curvisible) {
2082 lastvisible = curvisible;
2083 rebuildScreen(w);
2084 return;
2091 // ////////////////////////////////////////////////////////////////////////// //
2092 __gshared LockFile mainLockFile;
2095 void checkMainLockFile () {
2096 import std.path : buildPath;
2097 mainLockFile = LockFile(buildPath(mailRootDir, ".chiroptera.lock"));
2098 if (!mainLockFile.tryLock) { mainLockFile.close(); assert(0, "already running"); }
2102 void main (string[] args) {
2103 version(LDC) {
2104 // it is completely fucked
2105 pragma(msg, "MSG: fucked compiler");
2106 } else {
2107 import etc.linux.memoryerror;
2108 bool setMH = true;
2109 int idx = 1;
2110 while (idx < args.length) {
2111 string a = args[idx++];
2112 if (a == "--") break;
2113 if (a == "--gdb") {
2114 setMH = false;
2115 --idx;
2116 foreach (immutable c; idx+1..args.length) args[c-1] = args[c];
2119 if (setMH) registerMemoryErrorHandler();
2122 glconAllowOpenGLRender = false;
2124 checkMainLockFile();
2125 scope(exit) mainLockFile.close();
2127 sdpyWindowClass = "Chiroptera";
2128 //glconShowKey = "M-Grave";
2130 initConsole();
2131 hitwitInitConsole();
2133 clearBindings();
2134 setupDefaultBindings();
2136 concmd("exec chiroptera.rc tan");
2138 scanFolders();
2140 concmdf!"exec %s/accounts.rc tan"(mailRootDir);
2141 concmdf!"exec %s/addressbook.rc tan"(mailRootDir);
2142 concmdf!"exec %s/filters.rc tan"(mailRootDir);
2143 concmdf!"exec %s/highlights.rc tan"(mailRootDir);
2144 concmdf!"exec %s/twits.rc tan"(mailRootDir);
2145 concmdf!"exec %s/twit_threads.rc tan"(mailRootDir);
2146 concmdf!"exec %s/auto_twits.rc tan"(mailRootDir);
2147 concmdf!"exec %s/auto_twit_threads.rc tan"(mailRootDir);
2148 concmdf!"exec %s/repreps.rc tan"(mailRootDir);
2149 conProcessQueue(); // load config
2150 conProcessArgs!true(args);
2152 vbufEffScale = vbufNewScale;
2153 vbufEffVSync = vbufVSync;
2155 lastWinWidth = winWidthScaled;
2156 lastWinHeight = winHeightScaled;
2158 restoreCurrentFolderAndPosition();
2160 static if (EGfxOpenGLBackend) {
2161 vbwin = new SimpleWindow(lastWinWidth, lastWinHeight, "Chiroptera", OpenGlOptions.yes, Resizability.allowResizing);
2162 vbwin.hideCursor();
2163 } else {
2164 vbwin = new SimpleWindow(lastWinWidth, lastWinHeight, "Chiroptera", OpenGlOptions.no, Resizability.allowResizing);
2167 vbwin.onFocusChange = delegate (bool focused) {
2168 vbfocused = focused;
2169 if (!focused) {
2170 lastMouseButton = 0;
2171 eguiLostGlobalFocus();
2175 vbwin.windowResized = delegate (int wdt, int hgt) {
2176 // TODO: fix gui sizes
2177 if (vbwin.closed) return;
2179 if (lastWinWidth == wdt && lastWinHeight == hgt) return;
2180 glconResize(wdt, hgt);
2182 double glwFrac = cast(double)guiGroupListWidth/VBufWidth;
2183 double tlhFrac = cast(double)guiThreadListHeight/VBufHeight;
2185 if (wdt < vbufNewScale*32) wdt = vbufNewScale;
2186 if (hgt < vbufNewScale*32) hgt = vbufNewScale;
2187 int newwdt = (wdt+vbufNewScale-1)/vbufNewScale;
2188 int newhgt = (hgt+vbufNewScale-1)/vbufNewScale;
2190 guiGroupListWidth = cast(int)(glwFrac*newwdt+0.5);
2191 guiThreadListHeight = cast(int)(tlhFrac*newhgt+0.5);
2193 if (guiGroupListWidth < 12) guiGroupListWidth = 12;
2194 if (guiThreadListHeight < 16) guiThreadListHeight = 16;
2196 lastWinWidth = wdt;
2197 lastWinHeight = hgt;
2199 vglResizeBuffer(newwdt, newhgt, vbufNewScale);
2201 mouseMoved();
2203 rebuildScreen(vbwin);
2206 vbwin.addEventListener((DoConsoleCommandsEvent evt) {
2207 bool sendAnother = false;
2208 bool prevVisible = isConsoleVisible;
2210 consoleLock();
2211 scope(exit) consoleUnlock();
2212 conProcessQueue();
2213 sendAnother = !conQueueEmpty();
2215 if (sendAnother) postDoConCommands();
2216 if (vbwin.closed) return;
2217 if (isQuitRequested) { vbwin.close(); return; }
2218 if (prevVisible || isConsoleVisible) postScreenRepaintDelayed();
2221 vbwin.addEventListener((HideMouseEvent evt) {
2222 if (vbwin.closed) return;
2223 if (isQuitRequested) { vbwin.close(); return; }
2224 if (repostHideMouse) {
2225 //conwriteln("HME: visible=", isMouseVisible, "; alpha=", mouseAlpha, "; tm=", mshtime_dbg);
2226 if (mainPane !is null && !isMouseVisible) mainPane.lastUrlIndex = -1;
2227 repaintScreen(vbwin);
2231 vbwin.addEventListener((ScreenRebuildEvent evt) {
2232 if (vbwin.closed) return;
2233 if (isQuitRequested) { vbwin.close(); return; }
2234 rebuildScreen(vbwin);
2235 if (isConsoleVisible) postScreenRepaintDelayed();
2238 vbwin.addEventListener((ScreenRepaintEvent evt) {
2239 if (vbwin.closed) return;
2240 if (isQuitRequested) { vbwin.close(); return; }
2241 repaintScreen(vbwin);
2242 if (isConsoleVisible) postScreenRepaintDelayed();
2245 vbwin.addEventListener((CursorBlinkEvent evt) {
2246 if (vbwin.closed) return;
2247 rebuildScreen(vbwin);
2250 vbwin.addEventListener((QuitEvent evt) {
2251 if (vbwin.closed) return;
2252 if (isQuitRequested) { vbwin.close(); return; }
2253 vbwin.close();
2257 vbwin.addEventListener((UnreadChangedEvent evt) {
2258 if (vbwin.closed) return;
2259 if (isQuitRequested) { vbwin.close(); return; }
2260 setupTrayAnimation();
2263 vbwin.addEventListener((TrayStartAnimationEvent evt) {
2264 if (vbwin.closed) return;
2265 if (isQuitRequested) { vbwin.close(); return; }
2266 trayStartAnimation();
2269 vbwin.addEventListener((TrayStopAnimationEvent evt) {
2270 if (vbwin.closed) return;
2271 if (isQuitRequested) { vbwin.close(); return; }
2272 trayStopAnimation();
2275 vbwin.addEventListener((TraySetupAnimationEvent evt) {
2276 if (vbwin.closed) return;
2277 if (isQuitRequested) { vbwin.close(); return; }
2278 setupTrayAnimation();
2281 vbwin.addEventListener((TrayAnimationStepEvent evt) {
2282 if (vbwin.closed) return;
2283 if (isQuitRequested) { vbwin.close(); return; }
2284 trayDoAnimationStep();
2287 HintWindow uphintWindow;
2289 vbwin.addEventListener((UpdatingAccountEvent evt) {
2290 if (evt.accName.length) {
2291 if (uphintWindow !is null) {
2292 uphintWindow.message = "updating: "~evt.accName;
2293 } else {
2294 uphintWindow = new HintWindow("updating: "~evt.accName);
2295 uphintWindow.winy = guiThreadListHeight+1+(3*gxTextHeightUtf+2-uphintWindow.winh)/2;
2297 postScreenRebuild();
2301 vbwin.addEventListener((UpdatingAccountCompleteEvent evt) {
2302 if (evt.accName.length && uphintWindow !is null) {
2303 uphintWindow.message = "done: "~evt.accName;
2304 postScreenRebuild();
2308 vbwin.addEventListener((UpdatingCompleteEvent evt) {
2309 if (uphintWindow) {
2310 uphintWindow.close();
2311 uphintWindow = null;
2313 foreach (Folder fld; folders) if (fld.needRebuild) fld.buildVisibleList();
2314 setupTrayAnimation(); // check if we have to start/stop animation, and do it
2315 rebuildScreen(vbwin);
2318 vbwin.addEventListener((DoCheckBoxesCycleEvent evt) {
2319 //conwriteln("updating group '", groups[evt.gidx].mbase.groupname);
2320 if (!atomicLoad(updateInProgress)) {
2321 updateThreadId.send(UpThreadCommand.StartUpdate);
2325 vbwin.addEventListener((ArticleTextScrollEvent evt) {
2326 if (vbwin is null || vbwin.closed) return;
2327 if (mainPane is null) return;
2328 mainPane.doScrollStep();
2331 vbwin.addEventListener((MarkAsUnreadEvent evt) {
2332 if (vbwin is null || vbwin.closed) return;
2333 if (mainPane is null) return;
2334 //conwriteln("unread timer fired");
2335 if (evt.folder is getActiveFolder && evt.idx == evt.folder.curidx) {
2336 //conwriteln("*** unread timer hit!");
2337 evt.folder.curidxUnread = false;
2338 postScreenRebuild();
2342 vbwin.redrawOpenGlScene = delegate () {
2343 if (vbwin.closed) return;
2345 bool resizeWin = false;
2348 consoleLock();
2349 scope(exit) consoleUnlock();
2351 if (!conQueueEmpty()) postDoConCommands();
2353 if (vbufNewScale != vbufEffScale) {
2354 // window scale changed
2355 resizeWin = true;
2357 if (vbufEffVSync != vbufVSync) {
2358 vbufEffVSync = vbufVSync;
2359 vbwin.vsync = vbufEffVSync;
2363 if (resizeWin) {
2364 vbwin.resize(winWidthScaled, winHeightScaled);
2365 glconResize(winWidthScaled, winHeightScaled);
2366 //vglResizeBuffer(VBufWidth, VBufHeight, vbufNewScale);
2369 //if (rebuildTexture) repaintScreen();
2371 vglBlitTexture(vbwin);
2373 if (vArrowTextureId) {
2374 if (isMouseVisible) {
2375 int px = lastMouseX;
2376 int py = lastMouseY;
2377 //conwriteln("msalpha: ", mouseAlpha);
2378 glColor4f(1, 1, 1, mouseAlpha);
2379 vglBlitArrow(px, py);
2383 glconDraw();
2385 if (isQuitRequested()) vbwin.postEvent(new QuitEvent());
2388 static if (is(typeof(&vbwin.closeQuery))) {
2389 vbwin.closeQuery = delegate () { concmd("quit"); postDoConCommands(); };
2392 void firstTimeInit () {
2393 static bool firstTimeInited = false;
2394 if (firstTimeInited) return;
2395 firstTimeInited = true;
2397 static if (EGfxOpenGLBackend) {
2398 import iv.glbinds;
2399 vbwin.setAsCurrentOpenGlContext();
2400 vbwin.vsync = vbufEffVSync;
2402 vbufEffVSync = vbufVSync;
2404 vglResizeBuffer(VBufWidth, VBufHeight);
2405 vglCreateArrowTexture();
2407 glconInit(winWidthScaled, winHeightScaled);
2409 rebuildScreen(vbwin);
2411 updateThreadId = spawn(&updateThread, thisTid);
2413 // create notification icon
2414 if (trayicon is null) {
2415 auto drv = vfsAddPak(wrapMemoryRO(iconsZipData[]), "", "databinz/icons.zip:");
2416 scope(exit) vfsRemovePak(drv);
2417 try {
2418 foreach (immutable idx; 0..6) {
2419 string fname = "databinz/icons.zip:icons";
2420 if (idx == 0) {
2421 fname ~= "/main.png";
2422 } else {
2423 import std.format : format;
2424 fname = "%s/bat%s.png".format(fname, idx-1);
2426 auto fl = VFile(fname);
2427 if (fl.size == 0 || fl.size > 1024*1024) throw new Exception("fucked icon");
2428 auto pngraw = new ubyte[](cast(uint)fl.size);
2429 fl.rawReadExact(pngraw);
2430 auto img = readPng(pngraw);
2431 if (img is null) throw new Exception("fucked icon");
2432 icons[idx] = imageFromPng(img);
2434 foreach (immutable idx, MemoryImage img; icons[]) {
2435 trayimages[idx] = Image.fromMemoryImage(img);
2437 vbwin.icon = icons[0];
2438 trayicon = new NotificationAreaIcon("Chiroptera", trayimages[0], (MouseButton button) {
2439 scope(exit) if (!conQueueEmpty()) postDoConCommands();
2440 if (button == MouseButton.left) vbwin.switchToWindow();
2441 if (button == MouseButton.middle) concmd("quit");
2443 setupTrayAnimation();
2444 flushGui(); // or it may not redraw itself
2445 } catch (Exception e) {
2446 conwriteln("ERROR loading icons: ", e.msg);
2451 vbwin.visibleForTheFirstTime = delegate () {
2452 firstTimeInit();
2455 mainPane = new MainPaneWindow();
2457 postScreenRebuild();
2458 repostHideMouse();
2460 vbwin.eventLoop(1000*10,
2461 delegate () {
2462 scope(exit) if (!conQueueEmpty()) postDoConCommands();
2463 if (vbwin.closed) return;
2465 consoleLock();
2466 scope(exit) consoleUnlock();
2467 conProcessQueue();
2469 if (isQuitRequested) { vbwin.close(); return; }
2471 delegate (KeyEvent event) {
2472 scope(exit) if (!conQueueEmpty()) postDoConCommands();
2473 if (vbwin.closed) return;
2474 if (isQuitRequested) { vbwin.close(); return; }
2475 if (glconKeyEvent(event)) {
2476 postScreenRepaint();
2477 return;
2479 if ((event.modifierState&ModifierState.numLock) == 0) {
2480 switch (event.key) {
2481 case Key.Pad0: event.key = Key.Insert; break;
2482 case Key.Pad1: event.key = Key.End; break;
2483 case Key.Pad2: event.key = Key.Down; break;
2484 case Key.Pad3: event.key = Key.PageDown; break;
2485 case Key.Pad4: event.key = Key.Left; break;
2486 //case Key.Pad5: event.key = Key.Insert; break;
2487 case Key.Pad6: event.key = Key.Right; break;
2488 case Key.Pad7: event.key = Key.Home; break;
2489 case Key.Pad8: event.key = Key.Up; break;
2490 case Key.Pad9: event.key = Key.PageUp; break;
2491 case Key.PadEnter: event.key = Key.Enter; break;
2492 case Key.PadDot: event.key = Key.Delete; break;
2493 default: break;
2495 } else {
2496 if (event.key == Key.PadEnter) event.key = Key.Enter;
2498 if (dispatchEvent(event)) return;
2499 //postScreenRepaint(); // just in case
2501 delegate (MouseEvent event) {
2502 scope(exit) if (!conQueueEmpty()) postDoConCommands();
2503 if (vbwin.closed) return;
2504 lastMouseXUnscaled = event.x;
2505 lastMouseYUnscaled = event.y;
2506 if (event.type == MouseEventType.buttonPressed) lastMouseButton |= event.button;
2507 else if (event.type == MouseEventType.buttonReleased) lastMouseButton &= ~event.button;
2508 mouseMoved();
2509 if (dispatchEvent(event)) return;
2511 delegate (dchar ch) {
2512 if (vbwin.closed) return;
2513 scope(exit) if (!conQueueEmpty()) postDoConCommands();
2514 if (glconCharEvent(ch)) {
2515 postScreenRepaint();
2516 return;
2518 if (dispatchEvent(ch)) return;
2521 saveCurrentFolderAndPosition();
2522 trayimages[] = null;
2523 if (trayicon !is null && !trayicon.closed) { trayicon.close(); trayicon = null; }
2524 flushGui();
2525 updateThreadId.send(UpThreadCommand.Quit);
2526 conProcessQueue(int.max/4);