private filters; do not purge messages in active folder
[chiroptera.git] / chiroptera.d
blobd505f9ac3d557293754b694861dc11651c981b82
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 //version = test_round_rect;
20 version = article_can_into_html;
22 import core.atomic;
23 import core.time;
24 import std.concurrency;
26 //import arsd.email;
27 //import arsd.htmltotext;
28 import arsd.simpledisplay;
29 import arsd.png;
31 version(article_can_into_html) {
32 import arsd.characterencodings;
33 import arsd.color;
34 import arsd.dom;
35 import arsd.htmltotext;
38 import iv.alice;
39 import iv.bclamp;
40 import iv.encoding;
41 import iv.cmdcon;
42 import iv.cmdcongl;
43 import iv.lockfile;
44 import iv.sdpyutil;
45 import iv.strex;
46 import iv.sq3;
47 import iv.timer : DurTimer = Timer;
48 import iv.utfutil;
49 import iv.vfs.io;
50 import iv.vfs.util;
52 import iv.egra;
54 import chibackend;
55 import chibackend.net;
57 import chievents;
58 import dialogs;
59 import receiver;
62 // ////////////////////////////////////////////////////////////////////////// //
63 static immutable string ChiroStyle = `
64 MainPaneWindow {
65 //// group list ////
66 grouplist-divline: white;
67 grouplist-dots: rgb(80, 80, 80);
69 grouplist-back: #222;
70 // group with unread messages
71 grouplist-unread-text: #0ff;
72 // normal group
73 grouplist-normal-text: rgb(255, 187, 0);
74 grouplist-normal-child-text: rgb(225, 97, 0);
75 // spam group
76 grouplist-spam-text: #800;
77 // main accounts group
78 grouplist-accounts-text: rgb(220, 220, 0);
79 grouplist-accounts-child-text: rgb(160, 160, 250);
80 // account inbox group
81 grouplist-inbox-text: rgb(90, 90, 180);
84 grouplist-cursor-back: #088;
85 // group with unread messages
86 grouplist-cursor-unread-text: #0ff;
87 // normal group
88 grouplist-cursor-normal-text: rgb(255, 187, 0);
89 grouplist-cursor-normal-child-text: rgb(225, 97, 0);
90 // spam group
91 grouplist-cursor-spam-text: #800;
92 // main accounts group
93 grouplist-cursor-accounts-text: rgb(220, 220, 0);
94 grouplist-cursor-accounts-child-text: rgb(160, 160, 250);
95 // account inbox group
96 grouplist-cursor-inbox-text: rgb(90, 90, 180);
98 grouplist-cursor-outline: black;
100 //// thread list ////
101 threadlist-divline: white;
102 threadlist-back: #222;
103 threadlist-dots: #444;
105 threadlist-normal-back: transparent;
106 threadlist-normal-dots: #444;
107 threadlist-normal-subj-text: rgb(215, 87, 0);
108 threadlist-normal-from-text: rgb(215, 87, 0);
109 threadlist-normal-mail-text: rgb(155, 27, 0);
110 threadlist-normal-time-text: rgb(215, 87, 0);
111 threadlist-normal-strike-line: transparent;
113 threadlist-unread-back: transparent;
114 threadlist-unread-dots: #444;
115 threadlist-unread-subj-text: white;
116 threadlist-unread-from-text: white;
117 threadlist-unread-mail-text: yellow;
118 threadlist-unread-time-text: white;
119 threadlist-unread-strike-line: transparent;
121 threadlist-soft-del-back: transparent;
122 threadlist-soft-del-dots: #444;
123 threadlist-soft-del-subj-text: #800;
124 threadlist-soft-del-from-text: #800;
125 threadlist-soft-del-mail-text: #800;
126 threadlist-soft-del-time-text: #800;
127 threadlist-soft-del-strike-line: #800;
129 threadlist-hard-del-back: transparent;
130 threadlist-hard-del-dots: #444;
131 threadlist-hard-del-subj-text: red;
132 threadlist-hard-del-from-text: red;
133 threadlist-hard-del-mail-text: red;
134 threadlist-hard-del-time-text: red;
135 threadlist-hard-del-strike-line: red;
137 threadlist-twit-back: transparent;
138 threadlist-twit-dots: #444;
139 threadlist-twit-subj-text: #400;
140 threadlist-twit-from-text: #400;
141 threadlist-twit-mail-text: #400;
142 threadlist-twit-time-text: #400;
143 threadlist-twit-strike-line: transparent;
145 threadlist-cursor-normal-back: #088;
146 threadlist-cursor-dots: #444;
147 threadlist-cursor-normal-subj-text: rgb(215, 87, 0);
148 threadlist-cursor-normal-from-text: rgb(215, 87, 0);
149 threadlist-cursor-normal-mail-text: rgb(155, 27, 0);
150 threadlist-cursor-normal-time-text: rgb(215, 87, 0);
151 threadlist-cursor-normal-strike-line: transparent;
152 threadlist-cursor-normal-outline: black;
154 threadlist-cursor-unread-back: #088;
155 threadlist-cursor-unread-dots: #444;
156 threadlist-cursor-unread-subj-text: white;
157 threadlist-cursor-unread-from-text: white;
158 threadlist-cursor-unread-mail-text: yellow;
159 threadlist-cursor-unread-time-text: white;
160 threadlist-cursor-unread-strike-line: transparent;
161 threadlist-cursor-unread-outline: black;
163 threadlist-cursor-soft-del-back: #066;
164 threadlist-cursor-soft-del-dots: #444;
165 threadlist-cursor-soft-del-subj-text: #800;
166 threadlist-cursor-soft-del-from-text: #800;
167 threadlist-cursor-soft-del-mail-text: #800;
168 threadlist-cursor-soft-del-time-text: #800;
169 threadlist-cursor-soft-del-strike-line: #800;
171 threadlist-cursor-hard-del-back: #066;
172 threadlist-cursor-hard-del-dots: #444;
173 threadlist-cursor-hard-del-subj-text: red;
174 threadlist-cursor-hard-del-from-text: red;
175 threadlist-cursor-hard-del-mail-text: red;
176 threadlist-cursor-hard-del-time-text: red;
177 threadlist-cursor-hard-del-strike-line: red;
179 threadlist-cursor-twit-back: #066;
180 threadlist-cursor-twit-dots: #444;
181 threadlist-cursor-twit-subj-text: #400;
182 threadlist-cursor-twit-from-text: #400;
183 threadlist-cursor-twit-mail-text: #400;
184 threadlist-cursor-twit-time-text: #400;
185 threadlist-cursor-twit-strike-line: #400;
188 //// message header ////
189 msg-header-back: rgb(20, 20, 20);
190 msg-header-from: #088;
191 msg-header-to: #088;
192 msg-header-subj: #088;
193 msg-header-date: #088;
194 msg-header-divline: #bbb;
197 //// message text ////
198 msg-text-back: rgb(37, 37, 37);
199 msg-text-text: rgb(174, 174, 174);
200 msg-text-quote0: #880;
201 msg-text-quote1: #088;
202 msg-text-link: rgb(0, 200, 200);
203 msg-text-html-sign: rgb(128, 0, 128);
205 msg-text-link-hover: rgb(0, 255, 255);
207 msg-text-link-pressed: rgb(255, 0, 255);
209 //// message text twit ////
210 //twit-shade: rgba(0, 0, 80, 127);
211 twit-shade: rgb(0, 0, 40);
212 twit-text: red;
213 twit-outline: black;
217 HintWindow {
218 frame: white;
219 title-back: white;
220 title-text: black;
222 back: rgb(0, 0, 80);
223 text: rgb(155, 155, 155);
227 MessageWindow {
228 frame: white;
229 title-back: white;
230 title-text: black;
232 back: rgb(0, 0, 80);
233 text: rgb(255, 255, 0);
234 bar-back: rgb(90, 90, 180);
235 back: red;
240 // ////////////////////////////////////////////////////////////////////////// //
241 private __gshared bool ChiroTimerExEnabled = false;
244 // ////////////////////////////////////////////////////////////////////////// //
245 public __gshared string mailRootDir = "/mnt/bigass/Mail";
248 shared static this () {
249 import core.stdc.stdlib : getenv;
250 const(char)* home = getenv("HOME");
251 if (home !is null && home[0] == '/' && home[1] && home[1] != '/') {
252 import std.string : fromStringz;
253 mailRootDir = home.fromStringz.idup;
254 if (mailRootDir.length == 0) assert(0, "wtf?!");
255 if (mailRootDir[$-1] != '/') mailRootDir ~= "/";
256 mailRootDir ~= "Mail";
261 // ////////////////////////////////////////////////////////////////////////// //
262 __gshared NotificationAreaIcon trayicon;
263 __gshared Image[6] trayimages;
264 __gshared MemoryImage[6] icons; // 0: normal
267 // ////////////////////////////////////////////////////////////////////////// //
268 // with tagid
269 struct ArticleId {
270 uint tagid = 0;
271 uint uid = 0;
273 bool valid () const nothrow @safe @nogc { pragma(inline, true); return (tagid && uid); }
274 void clear () nothrow @safe @nogc { pragma(inline, true); tagid = uid = 0; }
276 bool opEqual (const ref ArticleId other) const nothrow @safe @nogc {
277 pragma(inline, true);
278 return (valid && other.valid && tagid == other.tagid && uid == other.uid);
283 // ////////////////////////////////////////////////////////////////////////// //
284 static immutable ubyte[] iconsZipData = cast(immutable(ubyte)[])import("databin/icons.zip");
287 // ////////////////////////////////////////////////////////////////////////// //
288 struct MailReplacement {
289 string oldmail;
290 string newmail;
291 string newname;
294 __gshared MailReplacement[string] repmailreps;
297 // ////////////////////////////////////////////////////////////////////////// //
298 string getBrowserCommand (bool forceOpera) {
299 __gshared string browser;
300 if (forceOpera) return "opera";
301 if (browser.length == 0) {
302 import core.stdc.stdlib : getenv;
303 const(char)* evar = getenv("BROWSER");
304 if (evar !is null && evar[0]) {
305 import std.string : fromStringz;
306 browser = evar.fromStringz.idup;
307 } else {
308 browser = "opera";
311 return browser;
315 // ////////////////////////////////////////////////////////////////////////// //
317 private void setXStr (ref char[] dest, SQ3Text src) {
318 delete dest;
319 if (src.length == 0) return;
320 dest = new char[src.length];
321 dest[] = src[];
326 // ////////////////////////////////////////////////////////////////////////// //
327 class FolderInfo {
328 uint tagid; // 0 means "ephemeral"
329 DynStr name;
330 const(char)[] visname; // slice of the `name`
331 int depth;
332 uint unreadCount;
333 // used in rescanner;
334 bool seen;
336 ~this () nothrow @trusted @nogc { clear(); }
338 void clear () nothrow @trusted @nogc {
339 visname = null;
340 name.clear();
343 // ephemeral folders doesn't exist, they are here only for visual purposes
344 bool ephemeral () const nothrow @safe @nogc { pragma(inline, true); return (tagid == 0); }
346 void calcDepthVisName () {
347 depth = 0;
348 visname = name[0..$];
349 if (visname.length == 0 || visname == "/" || visname[0] == '#') return;
350 visname = visname[1..$];
351 foreach (immutable char ch; visname) if (ch == '/') ++depth;
352 auto spos = visname.lastIndexOf('/');
353 if (spos >= 0) visname = visname[spos+1..$];
356 static int findByFullName (const(char)[] aname) {
357 if (aname.length == 0) return -1;
358 foreach (immutable idx, const FolderInfo fi; folderList) {
359 if (fi.name == aname) return cast(int)idx;
361 return -1;
364 bool needEphemeral () const {
365 if (depth == 0) return false;
366 assert(name.length > 1 && name[0] == '/');
367 const(char)[] n = name[0..$];
368 while (n.length) {
369 if (findByFullName(n) < 0) return true;
370 auto spos = n.lastIndexOf('/');
371 if (spos <= 0) return false;
372 n = n[0..spos];
374 return false;
377 void createEphemerals () const {
378 if (depth == 0) return;
379 assert(name.length > 1 && name[0] == '/');
380 const(char)[] n = name[0..$];
381 while (n.length) {
382 auto spos = n.lastIndexOf('/');
383 if (spos <= 0) break;
384 n = n[0..spos];
385 //conwriteln(" n=<", n, ">; spos=", spos);
386 if (findByFullName(n) < 0) {
387 //conwriteln(" creating: '", n, "'");
388 //foreach (const FolderInfo nfi; folderList) conwriteln(" <", nfi.name, "> : <", nfi.visname, "> : ", nfi.depth);
389 FolderInfo newfi = new FolderInfo;
390 newfi.tagid = 0;
391 newfi.name = n;
392 newfi.unreadCount = 0;
393 newfi.seen = true;
394 newfi.calcDepthVisName();
395 folderList ~= newfi;
401 __gshared FolderInfo[] folderList;
402 __gshared uint folderDataVersion = uint.max;
405 //FIXME: make this faster
406 //FIXME: force-append account folders
407 // returns `true` if something was changed
408 bool rescanFolders () {
409 import std.conv : to;
410 bool res = false;
412 if (folderList.length != 0 && dbView.getDataVersion() == folderDataVersion) return false; // nothing to do here
414 bool needsort = false;
415 foreach (FolderInfo fi; folderList) fi.seen = false;
417 // unhide any folder tags with unread messages
418 static stmtUnhide = LazyStatement!"View"(`
419 UPDATE tagnames
421 hidden=0
422 WHERE
423 hidden=1 AND
424 (tag='#spam' OR (tag<>'' AND SUBSTR(tag, 1, 1)='/')) AND
425 EXISTS (SELECT uid FROM threads WHERE tagid=tagnames.tagid AND appearance=`~(cast(int)Appearance.Unread).to!string~`)
426 ;`);
427 stmtUnhide.st.doAll();
429 static auto stmtGet = LazyStatement!"View"(`
430 SELECT
431 tagid AS tagid
432 , tag AS name
433 FROM tagnames
434 WHERE
435 tag='#spam' OR
436 (hidden=0 AND tag<>'' AND SUBSTR(tag, 1, 1)='/')
437 ;`);
439 static auto stmtGetUnread = LazyStatement!"View"(`
440 SELECT
441 COUNT(uid) AS unread
442 FROM threads
443 WHERE tagid=:tagid AND appearance=`~(cast(int)Appearance.Unread).to!string~`
444 ;`);
446 foreach (auto row; stmtGet.st.range) {
447 bool append = true;
448 uint tagid = row.tagid!uint;
449 foreach (FolderInfo fi; folderList) {
450 if (fi.tagid == tagid) {
451 append = false;
452 fi.seen = true;
453 if (fi.name != row.name!SQ3Text) {
454 fi.name = row.name!SQ3Text;
455 fi.calcDepthVisName();
456 needsort = true;
458 break;
461 if (append) {
462 needsort = true;
463 FolderInfo newfi = new FolderInfo();
464 newfi.tagid = tagid;
465 newfi.name = row.name!SQ3Text;
466 newfi.unreadCount = 0;
467 newfi.seen = true;
468 newfi.calcDepthVisName();
469 folderList ~= newfi;
473 // remove unseen folders
474 for (usize f = 0; f < folderList.length; ) {
475 if (!folderList[f].seen && !folderList[f].ephemeral) {
476 needsort = true;
477 folderList[f].clear();
478 delete folderList[f];
479 foreach (immutable c; f+1..folderList.length) folderList[c-1] = folderList[c];
480 folderList[$-1] = null;
481 folderList.length -= 1;
482 } else {
483 ++f;
487 if (needsort) {
488 // remove all epemerals
489 for (usize f = 0; f < folderList.length; ) {
490 if (folderList[f].ephemeral) {
491 folderList[f].clear();
492 delete folderList[f];
493 foreach (immutable c; f+1..folderList.length) folderList[c-1] = folderList[c];
494 folderList[$-1] = null;
495 folderList.length -= 1;
496 } else {
497 ++f;
501 // readd all ephemerals
502 for (;;) {
503 bool again = false;
504 foreach (FolderInfo fi; folderList) {
505 if (fi.needEphemeral) {
506 //conwriteln("ephemeral for '", fi.name, "'");
507 again = true;
508 fi.createEphemerals();
509 break;
512 if (!again) break;
515 static bool isAccount (const(char)[] s) pure nothrow @trusted @nogc {
516 if (!s.startsWith("/accounts")) return false;
517 return (s.length == 9 || s[9] == '/');
520 import std.algorithm.sorting : sort;
521 folderList.sort!((const FolderInfo a, const FolderInfo b) {
522 if (a.name == b.name) return false;
523 if (isAccount(a.name) && !isAccount(b.name)) return true; // a < b
524 if (!isAccount(a.name) && isAccount(b.name)) return false; // a >= b
525 if (a.name[0] == '#' && b.name[0] != '#') return false; // a >= b
526 if (a.name[0] != '#' && b.name[0] == '#') return true; // a < b
527 return (a.name < b.name);
529 res = true;
532 // check unread counts
533 foreach (FolderInfo fi; folderList) {
534 if (fi.ephemeral) continue;
535 foreach (auto row; stmtGetUnread.st.bind(":tagid", fi.tagid).range) {
536 if (fi.unreadCount != row.unread!uint) {
537 res = true;
538 fi.unreadCount = row.unread!uint;
543 setupTrayAnimation();
544 folderDataVersion = dbView.getDataVersion();
545 //conwriteln("ver=", folderDataVersion);
546 return res;
550 // ////////////////////////////////////////////////////////////////////////// //
551 __gshared bool dbg_dump_keynames;
554 // ////////////////////////////////////////////////////////////////////////// //
555 class TrayAnimationStepEvent {}
556 __gshared TrayAnimationStepEvent evTrayAnimationStep;
557 shared static this () { evTrayAnimationStep = new TrayAnimationStepEvent(); }
559 __gshared int trayAnimationIndex = 0; // 0: no animation
560 __gshared int trayAnimationDir = 1; // direction
561 __gshared uint trayAnimDataVersion = uint.max;
564 // ////////////////////////////////////////////////////////////////////////// //
565 void trayPostAnimationEvent () {
566 if (vbwin !is null && !vbwin.eventQueued!TrayAnimationStepEvent) vbwin.postTimeout(evTrayAnimationStep, 100);
570 void trayDoAnimationStep () {
571 if (trayicon is null || trayicon.closed) return; // no tray icon
572 if (vbwin is null || vbwin.closed) return;
573 if (trayAnimationIndex == 0) return; // no animation
574 trayPostAnimationEvent();
575 if (trayAnimationDir < 0) {
576 if (--trayAnimationIndex == 1) trayAnimationDir = 1;
577 } else {
578 if (++trayAnimationIndex == trayimages.length-1) trayAnimationDir = -1;
580 trayicon.icon = trayimages[trayAnimationIndex];
581 //vbwin.icon = icons[trayAnimationIndex];
582 //vbwin.sendDummyEvent(); // or it won't redraw itself
583 //flushGui(); // or it may not redraw itself
587 // ////////////////////////////////////////////////////////////////////////// //
588 void trayStartAnimation () {
589 if (trayicon is null) return; // no tray icon
590 if (trayAnimationIndex == 0) {
591 trayAnimationIndex = 1;
592 trayAnimationDir = 1;
593 trayicon.icon = trayimages[1];
594 vbwin.icon = icons[1];
595 flushGui(); // or it may not redraw itself
596 trayPostAnimationEvent();
601 void trayStopAnimation () {
602 if (trayicon is null) return; // no tray icon
603 if (trayAnimationIndex != 0) {
604 trayAnimationIndex = 0;
605 trayAnimationDir = 1;
606 trayicon.icon = trayimages[0];
607 vbwin.icon = icons[0];
608 flushGui(); // or it may not redraw itself
613 // check if we have to start/stop animation, and do it
614 void setupTrayAnimation () {
615 import std.conv : to;
616 static auto stmtGetUnread = LazyStatement!"View"(`
617 SELECT 1
618 FROM threads
619 WHERE appearance=`~(cast(int)Appearance.Unread).to!string~`
620 LIMIT 1
621 ;`);
623 if (trayicon is null) return; // no tray icon
625 immutable uint dver = dbView.getDataVersion();
626 //conwriteln("TRAY: dver=", dver, "; trv=", trayAnimDataVersion);
627 if (dver == trayAnimDataVersion) return;
628 trayAnimDataVersion = dver;
630 foreach (auto row; stmtGetUnread.st.range) {
631 //conwriteln("TRAY: start anim!");
632 trayStartAnimation();
633 return;
635 //conwriteln("TRAY: stop anim!");
636 trayStopAnimation();
640 // ////////////////////////////////////////////////////////////////////////// //
641 __gshared string[string] mainAppKeyBindings;
643 void clearBindings () {
644 mainAppKeyBindings.clear();
648 void mainAppBind (ConString kname, ConString concmd) {
649 KeyEvent evt = KeyEvent.parse(kname);
650 if (concmd.length) {
651 mainAppKeyBindings[evt.toStr] = concmd.idup;
652 } else {
653 mainAppKeyBindings.remove(evt.toStr);
658 void mainAppUnbind (ConString kname) {
659 KeyEvent evt = KeyEvent.parse(kname);
660 mainAppKeyBindings.remove(evt.toStr);
664 void setupDefaultBindings () {
665 //mainAppBind("C-L", "dbg_font_window");
666 mainAppBind("C-Q", "quit_prompt");
668 mainAppBind("N", "next_unread ona");
669 mainAppBind("S-N", "next_unread tan");
670 mainAppBind("M", "next_unread ona");
671 mainAppBind("S-M", "next_unread tan");
673 mainAppBind("U", "mark_unread");
674 mainAppBind("R", "mark_read");
676 mainAppBind("Space", "artext_page_down");
677 mainAppBind("S-Space", "artext_page_up");
678 mainAppBind("M-Up", "artext_line_up");
679 mainAppBind("M-Down", "artext_line_down");
681 mainAppBind("Up", "article_prev");
682 mainAppBind("Down", "article_next");
683 mainAppBind("PageUp", "article_pgup");
684 mainAppBind("PageDown", "article_pgdown");
685 mainAppBind("Home", "article_to_first");
686 mainAppBind("End", "article_to_last");
687 mainAppBind("C-Up", "article_scroll_up");
688 mainAppBind("C-Down", "article_scroll_down");
690 mainAppBind("C-PageUp", "folder_prev");
691 mainAppBind("C-PageDown", "folder_next");
693 mainAppBind("M-O", "folder_options");
695 //mainAppBind("C-M-U", "folder_update");
696 mainAppBind("C-S-I", "update_all");
697 mainAppBind("C-H", "article_dump_headers");
699 mainAppBind("C-Backslash", "find_mine");
701 //mainAppBind("C-Slash", "article_to_parent");
702 //mainAppBind("C-Comma", "article_to_prev_sib");
703 //mainAppBind("C-Period", "article_to_next_sib");
705 //mainAppBind("C-Insert", "article_copy_url_to_clipboard");
706 mainAppBind("C-M-K", "article_twit_thread");
707 mainAppBind("T", "article_edit_poster_title");
709 mainAppBind("C-R", "article_reply");
710 mainAppBind("S-R", "article_reply_to_from");
712 mainAppBind("S-P", "new_post");
714 //mainAppBind("S-Enter", "article_open_in_browser");
715 //mainAppBind("M-Enter", "article_open_in_browser tan");
717 mainAppBind("Delete", "article_softdelete_toggle");
718 mainAppBind("C-Delete", "article_harddelete_toggle");
722 // ////////////////////////////////////////////////////////////////////////// //
723 struct ImgViewCommand {
724 string filename;
727 private void imageViewThread (Tid ownerTid) {
728 string fname;
729 try {
730 conwriteln("waiting for the message...");
731 receive(
732 (ImgViewCommand cmd) {
733 fname = cmd.filename;
736 conwriteln("got filename: \"", fname, "\"");
738 try {
739 import std.process;
740 //FIXME: make regvar for image viewer
741 //auto pid = execute(["keh", attfname], null, Config.detached);
742 //pid.wait();
743 import std.stdio : File;
744 auto frd = File("/dev/null");
745 auto fwr = File("/dev/null", "w");
746 auto pid = spawnProcess(["keh", fname], frd, fwr, fwr, null, Config.none/*detached*/);
747 pid.wait();
748 } catch (Exception e) {
749 conwriteln("ERROR executing image viewer: ", e.msg);
751 } catch (Throwable e) {
752 // here, we are dead and fucked (the exact order doesn't matter)
753 //import core.stdc.stdlib : abort;
754 import core.stdc.stdio : fprintf, stderr;
755 //import core.memory : GC;
756 import core.thread : thread_suspendAll;
757 //GC.disable(); // yeah
758 //thread_suspendAll(); // stop right here, you criminal scum!
759 auto s = e.toString();
760 fprintf(stderr, "\n=== FATAL ===\n%.*s\n", cast(uint)s.length, s.ptr);
762 try {
763 import std.file : remove;
764 if (fname.length) {
765 conwriteln("deleting file \"", fname, "\"");
766 remove(fname);
768 } catch (Exception e) {}
772 // ////////////////////////////////////////////////////////////////////////// //
773 void initConsole () {
774 import std.functional : toDelegate;
776 conRegVar!scrollKeepLines("scroll_keep_lines", "number of lines to keep on page up or page down.");
777 conRegVar!unreadTimeoutInterval("t_unread_timeout", "timout to mark keyboard-selected message as unread (<1: don't mark)");
778 conRegVar!preferHtmlContent("prefer_html_content", "prefer html content when both html and plain are present?");
779 conRegVar!detectHtmlContent("detect_html_content", "detect html content in plain text?");
781 conRegFunc!clearBindings("binds_app_clear", "clear main application keybindings");
782 conRegFunc!setupDefaultBindings("binds_app_default", "*append* default application bindings");
783 conRegFunc!mainAppBind("bind_app", "add main application binding");
784 conRegFunc!mainAppUnbind("unbind_app", "remove main application binding");
787 // //////////////////////////////////////////////////////////////////// //
788 conRegFunc!((ConString filename) {
789 if (filename.length == 0) return;
790 import std.path : buildPath;
791 auto fname = buildPath(mailRootDir, filename);
792 char[] text;
793 scope(exit) delete text;
794 bool sayError = false;
795 try {
796 auto fl = VFile(fname);
797 sayError = true;
798 conwriteln("loading style file '", fname, "'...");
799 auto sz = fl.size;
800 if (sz > 1024*1024*32) { conwriteln("ERROR: style file too big!"); return; }
801 if (sz == 0) return;
802 text = new char[cast(usize)sz];
803 fl.rawReadExact(text);
804 fl.close();
805 } catch (Exception) {
806 if (sayError) conwriteln("ERROR reading style file!");
807 return;
809 try {
810 defaultColorStyle.parseStyle(text);
811 } catch (Exception e) {
812 conwriteln("ERROR parsing style: ", e.msg);
814 })("load_style", "load widget style");
817 // //////////////////////////////////////////////////////////////////// //
818 conRegFunc!(() {
819 auto qww = new YesNoWindow("Quit?", "Do you really want to quit?", true);
820 qww.onYes = () { concmd("quit"); };
821 //qww.addModal();
822 })("quit_prompt", "quit with prompt");
825 // //////////////////////////////////////////////////////////////////// //
826 conRegFunc!((ConString url, bool forceOpera=false) {
827 if (url.length) {
828 import std.stdio : File;
829 import std.process;
830 try {
831 auto frd = File("/dev/null");
832 auto fwr = File("/dev/null", "w");
833 spawnProcess([getBrowserCommand(forceOpera), url.idup], frd, fwr, fwr, null, Config.detached);
834 } catch (Exception e) {
835 conwriteln("ERROR executing URL viewer (", e.msg, ")");
838 })("open_url", "open given url in a browser");
841 // //////////////////////////////////////////////////////////////////// //
842 conRegFunc!(() {
843 receiverForceUpdateAll();
844 })("update_all", "mark all groups for updating");
847 // //////////////////////////////////////////////////////////////////// //
848 conRegFunc!(() {
849 if (mainPane !is null && mainPane.folderUpOne()) {
850 postScreenRebuild();
852 })("folder_prev", "go to previous group");
854 conRegFunc!(() {
855 if (mainPane !is null && mainPane.folderDownOne()) {
856 postScreenRebuild();
858 })("folder_next", "go to next group");
861 // //////////////////////////////////////////////////////////////////// //
862 conRegFunc!(() {
863 if (vbwin is null || vbwin.closed) return;
864 if (mainPane is null) return;
865 if (chiroGetMessageExactRead(mainPane.lastDecodedTid, mainPane.msglistCurrUId)) {
866 chiroSetMessageUnread(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
867 postScreenRebuild();
868 setupTrayAnimation();
870 })("mark_unread", "mark current message as unread");
872 conRegFunc!(() {
873 if (vbwin is null || vbwin.closed) return;
874 if (mainPane is null) return;
875 if (chiroGetMessageUnread(mainPane.lastDecodedTid, mainPane.msglistCurrUId)) {
876 chiroSetMessageRead(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
877 setupTrayAnimation();
878 postScreenRebuild();
880 })("mark_read", "mark current message as read");
882 conRegFunc!((bool allowNextGroup=false) {
883 if (vbwin is null || vbwin.closed) return;
884 if (mainPane is null) return;
885 // try current group
886 if (mainPane.lastDecodedTid != 0) {
887 auto uid = chiroGetPaneNextUnread(mainPane.msglistCurrUId);
888 if (uid) {
889 // i found her!
890 if (uid == mainPane.msglistCurrUId) return;
891 mainPane.msglistCurrUId = uid;
892 mainPane.threadListPositionDirty();
893 chiroSetMessageRead(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
894 setupTrayAnimation();
895 postScreenRebuild();
896 return;
899 // try other groups?
900 if (!allowNextGroup) return;
901 int idx = mainPane.folderCurrIndex;
902 foreach (; 0..cast(int)folderList.length) {
903 idx = (idx+1)%cast(int)folderList.length;
904 if (idx == mainPane.folderCurrIndex) continue;
905 if (folderList[idx].ephemeral) continue;
906 if (folderList[idx].unreadCount == 0) continue;
907 mainPane.folderSetToIndex(idx);
908 auto uid = chiroGetPaneNextUnread(/*mainPane.msglistCurrUId*/0);
909 if (uid) {
910 // i found her!
911 if (uid == mainPane.msglistCurrUId) return;
912 mainPane.msglistCurrUId = uid;
913 mainPane.threadListPositionDirty();
914 // don't mark the message as read
915 //chiroSetMessageRead(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
916 chiroSetMessageUnread(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
917 mainPane.setupUnreadTimer();
918 setupTrayAnimation();
919 postScreenRebuild();
920 return;
923 })("next_unread", "move to next unread message; bool arg: can skip to next group?");
926 // //////////////////////////////////////////////////////////////////// //
927 conRegFunc!(() {
928 if (mainPane !is null) mainPane.scrollByPageUp();
929 })("artext_page_up", "do pageup on article text");
931 conRegFunc!(() {
932 if (mainPane !is null) mainPane.scrollByPageDown();
933 })("artext_page_down", "do pagedown on article text");
935 conRegFunc!(() {
936 if (mainPane !is null) mainPane.scrollBy(-1);
937 })("artext_line_up", "do lineup on article text");
939 conRegFunc!(() {
940 if (mainPane !is null) mainPane.scrollBy(1);
941 })("artext_line_down", "do linedown on article text");
943 // //////////////////////////////////////////////////////////////////// //
944 /*FIXME
945 conRegFunc!((bool forceOpera=false) {
946 if (auto fldx = getActiveFolder) {
947 fldx.withBaseReader((abase, cur, top, alist) {
948 if (cur < alist.length) {
949 abase.loadContent(alist[cur]);
950 if (auto art = abase[alist[cur]]) {
951 scope(exit) art.releaseContent();
952 auto path = art.getHeaderValue("path:");
953 //conwriteln("path: [", path, "]");
954 if (path.startsWithCI("digitalmars.com!.POSTED.")) {
955 import std.stdio : File;
956 import std.process;
957 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
958 string id = art.msgid;
959 if (id.length > 2 && id[0] == '<' && id[$-1] == '>') id = id[1..$-1];
960 auto pid = spawnProcess(
961 [getBrowserCommand(forceOpera), "http://forum.dlang.org/post/"~id],
962 File("/dev/zero"), File("/dev/null"), File("/dev/null"),
964 pid.wait();
970 })("article_open_in_browser", "open the current article in browser (dlang forum)");
973 /*FIXME
974 conRegFunc!(() {
975 if (auto fldx = getActiveFolder) {
976 fldx.withBaseReader((abase, cur, top, alist) {
977 if (cur < alist.length) {
978 if (auto art = abase[alist[cur]]) {
979 auto path = art.getHeaderValue("path:");
980 if (path.startsWithCI("digitalmars.com!.POSTED.")) {
981 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
982 string id = art.msgid;
983 if (id.length > 2 && id[0] == '<' && id[$-1] == '>') id = id[1..$-1];
984 id = "http://forum.dlang.org/post/"~id;
985 setClipboardText(vbwin, id);
986 setPrimarySelection(vbwin, id);
992 })("article_copy_url_to_clipboard", "copy the url of the current article to clipboard (dlang forum)");
996 // //////////////////////////////////////////////////////////////////// //
997 conRegFunc!((ConString oldmail, ConString newmail, ConString newname) {
998 if (oldmail.length) {
999 if (newmail.length == 0) {
1000 repmailreps.remove(oldmail.idup);
1001 } else {
1002 MailReplacement mr;
1003 mr.oldmail = oldmail.idup;
1004 mr.newmail = newmail.idup;
1005 mr.newname = newname.idup;
1006 repmailreps[mr.oldmail] = mr;
1009 })("append_replyto_mail_replacement", "append replacement for reply mails");
1012 // //////////////////////////////////////////////////////////////////// //
1013 conRegFunc!(() {
1014 if (mainPane !is null && mainPane.threadListUp()) {
1015 postScreenRebuild();
1017 })("article_prev", "go to previous article");
1019 conRegFunc!(() {
1020 if (mainPane !is null && mainPane.threadListDown()) {
1021 postScreenRebuild();
1023 })("article_next", "go to next article");
1025 conRegFunc!(() {
1026 if (mainPane !is null && mainPane.threadListPageUp()) {
1027 postScreenRebuild();
1029 })("article_pgup", "artiles list: page up");
1031 conRegFunc!(() {
1032 if (mainPane !is null && mainPane.threadListPageDown()) {
1033 postScreenRebuild();
1035 })("article_pgdown", "artiles list: page down");
1037 conRegFunc!(() {
1038 if (mainPane !is null && mainPane.threadListScrollUp(movecurrent:false)) {
1039 postScreenRebuild();
1041 })("article_scroll_up", "scroll article list up");
1043 conRegFunc!(() {
1044 if (mainPane !is null && mainPane.threadListScrollDown(movecurrent:false)) {
1045 postScreenRebuild();
1047 })("article_scroll_down", "scroll article list up");
1049 conRegFunc!(() {
1050 if (mainPane !is null && mainPane.threadListHome()) {
1051 postScreenRebuild();
1053 })("article_to_first", "go to first article");
1055 conRegFunc!(() {
1056 if (mainPane !is null && mainPane.threadListEnd()) {
1057 postScreenRebuild();
1059 })("article_to_last", "go to last article");
1062 // //////////////////////////////////////////////////////////////////// //
1063 conRegFunc!(() {
1064 auto pw = new PostWindow();
1065 //pw.setFrom("goo boo!");
1066 pw.caption = "WRITE NEW MESSAGE";
1068 uint tid = mainPane.lastDecodedTid;
1069 if (tid) {
1070 dynstring tn = chiroGetTagName(tid);
1071 if (tn.length) {
1072 foreach (auto row; dbConf.statement(`
1073 SELECT name AS name, realname AS realname, email AS email, nntpgroup AS nntpgroup
1074 FROM accounts
1075 WHERE inbox=:inbox
1076 LIMIT 1
1077 ;`).bindConstText(":inbox", tn.getData).range)
1079 //pw.desttag = tn; // no need to
1080 pw.accname = row.name!SQ3Text;
1081 pw.setFrom(row.realname!SQ3Text, row.email!SQ3Text);
1082 pw.to.focus();
1086 })("new_post", "post a new article or message");
1089 // //////////////////////////////////////////////////////////////////// //
1090 static void doReplyXX (string repfld) {
1091 if (vbwin is null || vbwin.closed) return;
1092 if (mainPane is null) return;
1093 if (!mainPane.msglistCurrUId) return;
1095 auto pw = new PostWindow();
1096 pw.ed.focus();
1098 dynstring accname;
1099 foreach (auto row; dbView.statement(`
1100 SELECT DISTINCT(tagid) AS tagid, tt.tag AS name
1101 FROM threads
1102 INNER JOIN tagnames AS tt USING(tagid)
1103 WHERE uid=:uid AND name LIKE 'account:%'
1104 ;`).bind(":uid", mainPane.msglistCurrUId).range)
1106 auto name = row.name!SQ3Text;
1107 if (name.startsWith("account:")) {
1108 accname = name[8..$];
1109 if (accname.length) {
1110 pw.accname = accname;
1111 break;
1116 if (accname.length == 0) {
1117 conwriteln("ERROR: source account not found!");
1118 vbwin.beep();
1119 } else {
1120 conwriteln("account: ", accname);
1123 dynstring fldvalue;
1125 foreach (auto row; dbStore.statement(`
1126 SELECT ChiroHdr_Field(ChiroUnpack(data), :fldname) AS fldvalue
1127 FROM messages
1128 WHERE uid=:uid
1129 ORDER BY uid
1130 LIMIT 1
1131 `).bind(":uid", mainPane.msglistCurrUId).bindConstText(":fldname", repfld).range)
1133 fldvalue = row.fldvalue!SQ3Text.decodeSubj;
1136 if (fldvalue.length == 0 && !repfld.strEquCI("From")) {
1137 foreach (auto row; dbStore.statement(`
1138 SELECT ChiroHdr_Field(ChiroUnpack(data), :fldname) AS fldvalue
1139 FROM messages
1140 WHERE uid=:uid
1141 ORDER BY uid
1142 LIMIT 1
1143 `).bind(":uid", mainPane.msglistCurrUId).bindConstText(":fldname", "From").range)
1145 fldvalue = row.fldvalue!SQ3Text.decodeSubj;
1149 pw.to.str = fldvalue;
1150 dynstring cpt = dynstring("Reply to: ")~fldvalue;
1151 cpt ~= " (field: ";
1152 cpt ~= repfld;
1153 cpt ~= ")";
1154 pw.caption = cpt;
1156 dynstring fromname;
1157 foreach (auto row; dbView.statement(`
1158 SELECT subj AS subj, from_name AS fromname
1159 FROM info
1160 WHERE uid=:uid
1161 LIMIT 1
1162 `).bind(":uid", mainPane.msglistCurrUId).range)
1164 fromname = row.fromname!SQ3Text;
1165 dynstring s = "Re: ";
1166 s ~= row.subj!SQ3Text;
1167 pw.subj.str = s;
1170 foreach (auto row; dbView.statement(`
1171 SELECT ChiroUnpack(content) AS content
1172 FROM content_text
1173 WHERE uid=:uid
1174 LIMIT 1
1175 `).bind(":uid", mainPane.msglistCurrUId).range)
1177 pw.ed.addText(fromname);
1178 pw.ed.addText(" wrote:\n");
1179 pw.ed.addText("\n");
1180 dynstring text = row.content!SQ3Text;
1181 foreach (SQ3Text s; text.byLine) {
1182 if (s.length == 0 || s[0] == '>') pw.ed.addText(">"); else pw.ed.addText("> ");
1183 pw.ed.addText(s);
1184 pw.ed.addText("\n");
1186 pw.ed.addText("\n");
1187 pw.ed.reformat();
1190 foreach (auto row; dbConf.statement(`
1191 SELECT
1192 accid AS accid
1193 --, name AS name
1194 , recvserver AS recvserver
1195 , sendserver AS sendserver
1196 , realname AS realname
1197 , email AS email
1198 , nntpgroup AS nntpgroup
1199 FROM accounts
1200 WHERE name=:accname --AND sendserver<>'' AND email<>''
1201 LIMIT 1
1202 `).bindConstText(":accname", accname.getData).range)
1205 conwriteln("GROUP: <", row.nntpgroup!SQ3Text, ">");
1206 conwriteln("NAME: <", row.realname!SQ3Text, ">");
1207 conwriteln("EMAIL: <", row.email!SQ3Text, ">");
1209 dynstring frm;
1210 if (row.realname!SQ3Text.length) {
1211 frm = row.realname!SQ3Text;
1212 frm ~= " <";
1213 frm ~= row.email!SQ3Text;
1214 frm ~= ">";
1215 } else {
1216 frm = row.email!SQ3Text;
1218 pw.setFrom(frm);
1220 if (row.nntpgroup!SQ3Text.length) {
1221 pw.to.str = dynstring("newsgroup: ")~row.nntpgroup!SQ3Text;
1222 pw.to.readonly = true;
1223 pw.to.disabled = true;
1224 pw.allowAccountChange = false;
1225 pw.nntp = true;
1229 writeln("getting msgid...");
1230 foreach (auto row; dbView.statement(`
1231 SELECT msgid AS msgid
1232 FROM msgids
1233 WHERE uid=:uid
1234 LIMIT 1
1235 `).bind(":uid", mainPane.msglistCurrUId).range)
1237 conwriteln("MSGID: |", row.msgid!SQ3Text);
1238 if (pw.references.length) pw.references ~= " ";
1239 // two times, one for references field
1240 if (pw.references.length) pw.references ~= " ";
1241 pw.references ~= row.msgid!SQ3Text;
1242 if (pw.references.length) pw.references ~= " ";
1243 pw.references ~= row.msgid!SQ3Text;
1246 writeln("select references...");
1247 foreach (auto row; dbView.statement(`
1248 SELECT msgid AS msgid
1249 FROM refids
1250 WHERE uid=:uid
1251 ORDER BY idx
1252 `).bind(":uid", mainPane.msglistCurrUId).range)
1254 conwriteln("REF: |", row.msgid!SQ3Text);
1255 if (pw.references.length) pw.references ~= " ";
1256 pw.references ~= row.msgid!SQ3Text;
1259 pw.desttag = chiroGetTagName(mainPane.lastDecodedTid);
1262 static void doReply (string repfld) {
1263 try {
1264 doReplyXX(repfld);
1265 } catch (Exception e) {
1266 conwriteln("EXCEPTION: ", e.msg);
1267 auto s = e.toString();
1268 conwriteln(s);
1272 conRegFunc!(() { doReply("Reply-To"); })("article_reply", "reply to the current article with \"reply-to\" field");
1273 conRegFunc!(() { doReply("From"); })("article_reply_to_from", "reply to the current article with \"from\" field");
1276 // //////////////////////////////////////////////////////////////////// //
1277 conRegFunc!(() {
1278 if (vbwin is null || vbwin.closed) return;
1279 if (mainPane is null) return;
1280 if (mainPane.msglistCurrUId) {
1281 //messageBogoMarkHam(mainPane.msglistCurrUId);
1282 foreach (auto mrow; dbStore.statement(`SELECT ChiroUnpack(data) AS data FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", mainPane.msglistCurrUId).range) {
1283 try {
1284 import core.stdc.stdio : snprintf;
1285 char[128] fname = void;
1286 auto len = snprintf(fname.ptr, fname.length, "~/Mail/_bots/z_eml_%u.eml", cast(uint)mainPane.msglistCurrUId);
1287 auto fo = VFile(fname[0..len], "w");
1288 fo.writeln(mrow.data!SQ3Text.xstripright);
1289 fo.close();
1290 conwriteln("article exported to: ", fname[0..len]);
1291 } catch (Exception e) {
1295 })("article_export", "export current article as raw text");
1298 conRegFunc!(() {
1299 if (vbwin is null || vbwin.closed) return;
1300 if (mainPane is null) return;
1301 if (mainPane.msglistCurrUId) {
1302 messageBogoMarkHam(mainPane.msglistCurrUId);
1303 //TODO: move out of spam
1305 })("article_mark_ham", "mark current article as ham");
1308 conRegFunc!(() {
1309 if (vbwin is null || vbwin.closed) return;
1310 if (mainPane is null) return;
1311 if (mainPane.msglistCurrUId) {
1312 immutable uint uid = mainPane.msglistCurrUId;
1313 // move to the next message
1314 if (!mainPane.threadListDown()) mainPane.threadListUp();
1315 conwriteln("calling bogofilter...");
1316 messageBogoMarkSpam(uid);
1317 conwriteln("adding '#spam' tag");
1318 immutable bool updatePane = chiroMessageAddTag(uid, "#spam");
1319 conwriteln("removing other virtual folder tags...");
1320 DynStr[] tags;
1321 scope(exit) delete tags;
1322 tags.reserve(32);
1323 static auto stGetMsgTags = LazyStatement!"View"(`
1324 SELECT DISTINCT(tagid) AS tagid, tt.tag AS name
1325 FROM threads
1326 INNER JOIN tagnames AS tt USING(tagid)
1327 WHERE uid=:uid
1328 ;`);
1329 foreach (auto row; stGetMsgTags.st.bind(":uid", uid).range) {
1330 auto tag = row.name!SQ3Text;
1331 conwriteln(" tag: ", tag);
1332 if (tag.startsWith("/")) tags ~= DynStr(tag);
1334 foreach (ref DynStr tn; tags) {
1335 conwriteln("removing tag '", tn.getData, "'...");
1336 chiroMessageRemoveTag(uid, tn.getData);
1338 conwriteln("done marking as spam.");
1339 if (updatePane) {
1340 mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
1341 } else {
1342 // rescan folder
1343 mainPane.updateViewsIfTid(chiroGetTagUid("#spam"));
1345 postScreenRebuild();
1347 })("article_mark_spam", "mark current article as spam");
1350 // //////////////////////////////////////////////////////////////////// //
1351 conRegFunc!((ConString fldname) {
1352 if (vbwin is null || vbwin.closed) return;
1353 if (mainPane is null) return;
1354 if (!mainPane.msglistCurrUId) return;
1355 immutable uint uid = mainPane.msglistCurrUId;
1357 fldname = fldname.xstrip;
1358 while (fldname.length && fldname[$-1] == '/') fldname = fldname[0..$-1].xstrip;
1359 immutable bool isforced = (fldname.length && fldname[0] == '!');
1360 if (isforced) fldname = fldname[1..$];
1361 if (fldname.length == 0) {
1362 conwriteln("ERROR: cannot move to empty folder");
1363 return;
1365 if (fldname[0].isalnum) {
1366 conwriteln("ERROR: invalid folder name '", fldname, "'");
1367 return;
1370 uint tagid;
1371 if (!isforced) {
1372 tagid = chiroGetTagUid(fldname);
1373 } else {
1374 tagid = chiroAppendTag(fldname, hidden:(fldname[0] == '#' ? 1 : 0));
1376 if (!tagid) {
1377 conwriteln("ERROR: invalid folder name '", fldname, "'");
1378 return;
1381 immutable bool updatePane = chiroMessageAddTag(uid, fldname);
1382 DynStr[] tags;
1383 scope(exit) delete tags;
1384 tags.reserve(32);
1385 static auto stGetMsgTags = LazyStatement!"View"(`
1386 SELECT DISTINCT(tagid) AS tagid, tt.tag AS name
1387 FROM threads
1388 INNER JOIN tagnames AS tt USING(tagid)
1389 WHERE uid=:uid
1390 ;`);
1391 foreach (auto row; stGetMsgTags.st.bind(":uid", uid).range) {
1392 auto tag = row.name!SQ3Text;
1393 conwriteln(" tag: ", tag);
1394 if (tag.startsWith("account:")) continue;
1395 if (tag != fldname) tags ~= DynStr(tag);
1397 foreach (ref DynStr tn; tags) {
1398 conwriteln("removing tag '", tn.getData, "'...");
1399 chiroMessageRemoveTag(uid, tn.getData);
1401 if (updatePane) {
1402 mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
1403 } else {
1404 // rescan folder
1405 mainPane.updateViewsIfTid(chiroGetTagUid("#spam"));
1407 postScreenRebuild();
1408 })("article_move_to_folder", "move article to existing folder");
1411 // //////////////////////////////////////////////////////////////////// //
1412 version(none) {
1413 conRegFunc!(() {
1414 /*FIXME
1415 if (auto fld = getActiveFolder) {
1416 fld.moveToParent();
1417 postScreenRebuild();
1420 })("article_to_parent", "jump to parent article, if any");
1422 conRegFunc!(() {
1423 /*FIXME
1424 if (auto fld = getActiveFolder) {
1425 fld.moveToPrevSib();
1426 postScreenRebuild();
1429 })("article_to_prev_sib", "jump to previous sibling");
1431 conRegFunc!(() {
1432 /*FIXME
1433 if (auto fld = getActiveFolder) {
1434 fld.moveToNextSib();
1435 postScreenRebuild();
1438 })("article_to_next_sib", "jump to next sibling");
1441 // //////////////////////////////////////////////////////////////////// //
1442 conRegFunc!(() {
1443 if (vbwin is null || vbwin.closed) return;
1444 if (mainPane is null) return;
1445 if (mainPane.lastDecodedTid == 0 || mainPane.msglistCurrUId == 0) return;
1446 DynStr tagname = chiroGetTagName(mainPane.lastDecodedTid);
1447 if (!globmatch(tagname.getData, "/dmars_ng/*")) return;
1448 // get from
1449 DynStr fromMail, fromName;
1450 if (!chiroGetMessageFrom(mainPane.msglistCurrUId, ref fromMail, ref fromName)) return;
1451 if (fromMail.length == 0 && fromName.length == 0) return;
1452 //writeln("!!! email=", fromMail.getData, "; name=", fromName.getData, "|");
1454 uint twitid = 0;
1455 DynStr twtitle;
1456 DynStr twname;
1457 DynStr twemail;
1458 DynStr twnotes;
1459 DynStr twtagglob = "/dmars_ng/*";
1460 bool withName = false;
1462 static auto stFindTwit = LazyStatement!"Conf"(`
1463 SELECT
1464 etwitid AS twitid
1465 , tagglob AS tagglob
1466 , email AS email
1467 , name AS name
1468 , title AS title
1469 , notes AS notes
1470 FROM emailtwits
1471 WHERE email=:email AND name=:name
1472 ;`);
1474 static auto stAddTwit = LazyStatement!"Conf"(`
1475 INSERT INTO emailtwits
1476 ( tagglob, email, name, title, notes)
1477 VALUES(:tagglob,:email,:name,:title,:notes)
1478 ;`);
1480 static auto stModifyTwit = LazyStatement!"Conf"(`
1481 UPDATE emailtwits
1483 tagglob=:tagglob
1484 , email=:email
1485 , name=:name
1486 , title=:title
1487 , notes=:notes
1488 WHERE etwitid=:twitid
1489 ;`);
1491 static auto stRemoveTwitAuto = LazyStatement!"Conf"(`
1492 DELETE FROM msgidtwits
1493 WHERE etwitid=:twitid
1494 ;`);
1496 static auto stRemoveTwit = LazyStatement!"Conf"(`
1497 DELETE FROM emailtwits
1498 WHERE etwitid=:twitid
1499 ;`);
1501 stFindTwit.st
1502 .bindConstText(":email", fromMail.getData)
1503 .bindConstText(":name", fromName.getData);
1504 foreach (auto row; stFindTwit.st.range) {
1505 //conwriteln("!!!", row.twitid!uint, "; mail=", row.email!SQ3Text, "; name=", row.name!SQ3Text, "; title=", row.title!SQ3Text.recodeToKOI8);
1506 if (!globmatch(tagname.getData, row.tagglob!SQ3Text)) continue;
1507 twitid = row.twitid!uint;
1508 twtitle = row.title!SQ3Text;
1509 twname = row.name!SQ3Text;
1510 twemail = row.email!SQ3Text;
1511 twtagglob = row.tagglob!SQ3Text;
1512 twnotes = row.notes!SQ3Text;
1513 withName = (fromName.length != 0);
1514 break;
1517 if (!twitid && fromName.length) {
1518 stFindTwit.st
1519 .bindConstText(":email", fromMail.getData)
1520 .bindConstText(":name", "");
1521 foreach (auto row; stFindTwit.st.range) {
1522 //conwriteln("!!!", row.twitid!uint, "; mail=", row.email!SQ3Text, "; name=", row.name!SQ3Text, "; title=", row.title!SQ3Text.recodeToKOI8);
1523 if (!globmatch(tagname.getData, row.tagglob!SQ3Text)) continue;
1524 twitid = row.twitid!uint;
1525 twtitle = row.title!SQ3Text;
1526 twname = row.name!SQ3Text;
1527 twemail = row.email!SQ3Text;
1528 twtagglob = row.tagglob!SQ3Text;
1529 twnotes = row.notes!SQ3Text;
1530 withName = false;
1531 break;
1535 //conwriteln("twitid: ", twitid, "; title=", twtitle.getData.recodeToKOI8);
1537 auto tw = new TitlerWindow((twitid ? twname : fromName), (twitid ? twemail : fromMail), twtagglob, twtitle);
1538 tw.onSelected = delegate (name, email, glob, title) {
1539 if (email.length == 0 && name.length == 0) return false;
1540 if (glob.length == 0) return false;
1541 if (email.length && email[0] == '@') return false;
1542 title = title.xstrip;
1543 if (twitid) {
1544 if (title.length == 0) {
1545 // remove twit
1546 conwriteln("removing twit...");
1547 stRemoveTwitAuto.st.bind(":twitid", twitid).doAll();
1548 stRemoveTwit.st.bind(":twitid", twitid).doAll();
1549 } else {
1550 // change twit
1551 conwriteln("changing twit...");
1552 stModifyTwit.st
1553 .bind(":twitid", twitid)
1554 .bindConstText(":tagglob", glob)
1555 .bindConstText(":email", email)
1556 .bindConstText(":name", name)
1557 .bindConstText(":title", title)
1558 .bindConstText(":notes", twnotes.getData, allowNull:true)
1559 .doAll();
1561 } else {
1562 if (title.length == 0) return false;
1563 // new twit
1564 conwriteln("adding twit...");
1565 stAddTwit.st
1566 .bindConstText(":tagglob", glob)
1567 .bindConstText(":email", email)
1568 .bindConstText(":name", name)
1569 .bindConstText(":title", title)
1570 .bindConstText(":notes", null, allowNull:true)
1571 .doAll();
1574 if (vbwin && !vbwin.closed) vbwin.postEvent(new RecalcAllTwitsEvent());
1575 return true;
1577 })("article_edit_poster_title", "edit poster's title of the current article");
1580 // //////////////////////////////////////////////////////////////////// //
1581 conRegFunc!(() {
1582 if (vbwin is null || vbwin.closed) return;
1583 if (mainPane is null) return;
1584 if (mainPane.lastDecodedTid == 0 || mainPane.msglistCurrUId == 0) return;
1585 DynStr tagname = chiroGetTagName(mainPane.lastDecodedTid);
1586 if (!globmatch(tagname.getData, "/dmars_ng/*")) return;
1587 int mute = chiroGetMessageMute(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
1588 // check for non-automatic mutes
1589 if (mute != Mute.Normal && mute <= Mute.ThreadStart) return;
1590 createTwitByMsgid(mainPane.msglistCurrUId);
1591 mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
1592 postScreenRebuild();
1593 })("article_twit_thread", "twit current thread");
1595 conRegFunc!(() {
1596 if (vbwin is null || vbwin.closed) return;
1597 if (mainPane is null) return;
1598 int app = chiroGetMessageAppearance(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
1599 if (app >= 0) {
1600 //conwriteln("oldapp: ", app, " (", isSoftDeleted(app), ")");
1601 immutable bool wasPurged = (app == Appearance.SoftDeletePurge);
1602 app =
1603 app == Appearance.SoftDeletePurge ? Appearance.SoftDeleteUser :
1604 isSoftDeleted(app) ? Appearance.Read :
1605 Appearance.SoftDeleteUser;
1606 //conwriteln("newapp: ", app);
1607 chiroSetMessageAppearance(mainPane.lastDecodedTid, mainPane.msglistCurrUId, cast(Appearance)app);
1608 if (!wasPurged && isSoftDeleted(app)) mainPane.threadListDown();
1609 postScreenRebuild();
1611 })("article_softdelete_toggle", "toggle \"soft deleted\" flag on current article");
1613 conRegFunc!(() {
1614 if (vbwin is null || vbwin.closed) return;
1615 if (mainPane is null) return;
1616 int app = chiroGetMessageAppearance(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
1617 if (app >= 0) {
1618 //conwriteln("oldapp: ", app);
1619 app =
1620 app == Appearance.SoftDeletePurge ? Appearance.Read :
1621 isSoftDeleted(app) ? Appearance.SoftDeletePurge :
1622 Appearance.SoftDeletePurge;
1623 //conwriteln("newapp: ", app);
1624 chiroSetMessageAppearance(mainPane.lastDecodedTid, mainPane.msglistCurrUId, cast(Appearance)app);
1625 if (app == Appearance.SoftDeletePurge) mainPane.threadListDown();
1626 postScreenRebuild();
1628 })("article_harddelete_toggle", "toggle \"hard deleted\" flag on current article");
1631 // //////////////////////////////////////////////////////////////////// //
1632 conRegFunc!(() {
1633 if (vbwin is null || vbwin.closed) return;
1634 if (mainPane is null) return;
1635 DynStr hdrs = chiroMessageHeaders(mainPane.msglistCurrUId);
1636 if (hdrs.length == 0) return;
1637 conwriteln("============================");
1638 conwriteln("message uid: ", mainPane.msglistCurrUId);
1639 conwriteln(" tag tagid: ", mainPane.lastDecodedTid);
1640 conwriteln("============================");
1641 forEachHeaderLine(hdrs.getData, (const(char)[] line) {
1642 conwriteln(" ", line.xstripright);
1643 return true; // go on
1645 })("article_dump_headers", "dump article headers");
1647 conRegFunc!(() {
1648 if (vbwin is null || vbwin.closed) return;
1649 if (mainPane is null) return;
1650 DynStr hdrs = chiroMessageHeaders(mainPane.msglistCurrUId);
1651 if (hdrs.length == 0) return;
1652 conwriteln("============================");
1653 conwriteln("message uid: ", mainPane.msglistCurrUId);
1654 conwriteln(" tag tagid: ", mainPane.lastDecodedTid);
1655 conwriteln("============================");
1656 auto bogo = messageBogoCheck(mainPane.msglistCurrUId);
1657 conwriteln("BOGO RESULT: ", bogo);
1658 })("article_bogo_check", "check article with bogofilter (purely informational)");
1660 conRegFunc!(() {
1661 if (mainPane !is null && mainPane.lastDecodedTid) {
1662 conwriteln("relinking ", mainPane.lastDecodedTid, " (", chiroGetTagName(mainPane.lastDecodedTid).getData, ")...");
1663 chiroSupportRelinkTagThreads(mainPane.lastDecodedTid);
1664 mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
1665 postScreenRebuild();
1667 })("folder_rebuild_index", "rebuild thread index");
1669 conRegFunc!(() {
1670 if (vbwin && !vbwin.closed && mainPane !is null /*&& mainPane.lastDecodedTid*/) {
1671 vbwin.postEvent(new RecalcAllTwitsEvent());
1673 })("folder_rebuild_twits", "rebuild all twits");
1675 conRegFunc!(() {
1676 if (mainPane !is null && mainPane.folderCurrTag.length) {
1677 auto w = new TagOptionsWindow(mainPane.folderCurrTag.getData);
1678 w.onUpdated = delegate void (tagname) {
1679 if (vbwin && !vbwin.closed && mainPane !is null && mainPane.lastDecodedTagName == tagname) {
1680 mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
1683 postScreenRebuild();
1685 })("folder_options", "show options window");
1688 conRegFunc!(() {
1689 if (auto fld = getActiveFolder) {
1690 fld.withBase(delegate (abase) {
1691 uint idx = fld.curidx;
1692 if (idx >= fld.length) {
1693 idx = 0;
1694 } else if (auto art = abase[fld.baseidx(idx)]) {
1695 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
1696 idx = (idx+1)%fld.length;
1699 foreach (immutable _; 0..fld.length) {
1700 auto art = abase[fld.baseidx(idx)];
1701 if (art !is null) {
1702 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
1703 fld.curidx = cast(int)idx;
1704 postScreenRebuild();
1705 return;
1708 idx = (idx+1)%fld.length;
1712 })("find_mine", "find mine article");
1715 // //////////////////////////////////////////////////////////////////// //
1716 conRegVar!dbg_dump_keynames("dbg_dump_key_names", "dump key names");
1719 // //////////////////////////////////////////////////////////////////// //
1720 conRegFunc!((uint idx) {
1721 if (vbwin is null || vbwin.closed) return;
1722 if (mainPane is null) return;
1723 if (mainPane.msglistCurrUId == 0) return;
1725 static auto statAttaches = LazyStatement!"View"(`
1726 SELECT
1727 idx AS idx
1728 , mime AS mime
1729 , name AS name
1730 , ChiroUnpack(content) AS content
1731 FROM attaches
1732 WHERE uid=:uid
1733 ORDER BY idx
1734 ;`);
1735 char[128] buf = void;
1736 uint attidx = 0;
1737 foreach (auto row; statAttaches.st.bind(":uid", mainPane.msglistCurrUId).range) {
1738 import core.stdc.stdio : snprintf;
1739 if (idx != 0) { --idx; ++attidx; continue; }
1740 if (!row.mime!SQ3Text.startsWith("image")) return;
1741 auto name = row.name!SQ3Text.getExtension;
1742 if (name.length == 0) return;
1743 try {
1744 DynStr fname;
1745 fname = "/tmp/_viewimage_";
1747 import std.uuid;
1748 UUID id = randomUUID();
1749 foreach (immutable ubyte b; id.data[]) {
1750 fname ~= "0123456789abcdef"[b>>4];
1751 fname ~= "0123456789abcdef"[b&0x0f];
1753 fname ~= name;
1755 conwriteln("writing attach #", attidx, " to '", fname.getData, "'");
1756 VFile fo = VFile(fname.getData, "w");
1757 fo.rawWriteExact(row.content!SQ3Text);
1758 fo.close();
1760 auto vtid = spawn(&imageViewThread, thisTid);
1761 string fnamestr = fname.getData.idup;
1762 vtid.send(ImgViewCommand(fnamestr));
1763 } catch (Exception e) {
1764 conwriteln("ERROR writing attachment: ", e.msg);
1766 break;
1768 })("attach_view", "view attached image: attach_view index");
1771 // //////////////////////////////////////////////////////////////////// //
1772 conRegFunc!((uint idx, ConString userfname=null) {
1773 if (vbwin is null || vbwin.closed) return;
1774 if (mainPane is null) return;
1775 if (mainPane.msglistCurrUId == 0) return;
1777 static auto statAttaches = LazyStatement!"View"(`
1778 SELECT
1779 idx AS idx
1780 , mime AS mime
1781 , name AS name
1782 , ChiroUnpack(content) AS content
1783 FROM attaches
1784 WHERE uid=:uid
1785 ORDER BY idx
1786 ;`);
1787 char[128] buf = void;
1788 uint attidx = 0;
1789 foreach (auto row; statAttaches.st.bind(":uid", mainPane.msglistCurrUId).range) {
1790 import core.stdc.stdio : snprintf;
1791 if (idx != 0) { --idx; ++attidx; continue; }
1792 try {
1793 auto name = row.name!SQ3Text/*.decodeSubj*/;
1794 DynStr fname;
1796 if (userfname.length) {
1797 fname = userfname;
1798 } else {
1799 fname = "/tmp/";
1800 name = name.sanitizeFileNameStr;
1801 if (name.length == 0) {
1802 auto len = snprintf(buf.ptr, buf.sizeof, "attach_%02u.bin", attidx);
1803 fname ~= buf[0..cast(usize)len];
1804 } else {
1805 fname ~= name;
1809 conwriteln("writing attach #", attidx, " to '", fname.getData, "'");
1810 VFile fo = VFile(fname.getData, "w");
1811 fo.rawWriteExact(row.content!SQ3Text);
1812 fo.close();
1813 } catch (Exception e) {
1814 conwriteln("ERROR writing attachment: ", e.msg);
1816 break;
1818 })("attach_save", "save attach: attach_save index [filename]");
1822 // ////////////////////////////////////////////////////////////////////////// //
1823 //FIXME: turn into property
1824 final class ArticleTextScrollEvent {}
1825 __gshared ArticleTextScrollEvent evArticleScroll;
1826 shared static this () {
1827 evArticleScroll = new ArticleTextScrollEvent();
1830 void postArticleScroll () {
1831 if (vbwin !is null && !vbwin.eventQueued!ArticleTextScrollEvent) vbwin.postTimeout(evArticleScroll, 25);
1835 // ////////////////////////////////////////////////////////////////////////// //
1836 class MarkAsUnreadEvent {
1837 uint tagid;
1838 uint uid;
1841 __gshared int unreadTimeoutInterval = 600;
1842 __gshared int scrollKeepLines = 3;
1843 __gshared bool preferHtmlContent = false;
1844 __gshared bool detectHtmlContent = true;
1847 void postMarkAsUnreadEvent () {
1848 if (unreadTimeoutInterval > 0 && unreadTimeoutInterval < 10000 && vbwin !is null && mainPane !is null) {
1849 if (!mainPane.msglistCurrUId) return;
1850 if (chiroGetMessageUnread(mainPane.lastDecodedTid, mainPane.msglistCurrUId)) {
1851 //conwriteln("setting new unread timer");
1852 auto evt = new MarkAsUnreadEvent();
1853 evt.tagid = mainPane.lastDecodedTid;
1854 evt.uid = mainPane.msglistCurrUId;
1855 vbwin.postTimeout(evt, unreadTimeoutInterval);
1861 // ////////////////////////////////////////////////////////////////////////// //
1862 __gshared MainPaneWindow mainPane;
1865 final class MainPaneWindow : SubWindow {
1866 dynstring[] emlines; // in utf
1867 uint emlinesBeforeAttaches;
1868 uint lastDecodedUId;
1869 uint lastDecodedTid; // for which folder we last build our view?
1870 uint lastMaxTidUid;
1871 DynStr lastDecodedTagName;
1872 DynStr arttoname;
1873 DynStr arttomail;
1874 DynStr artfromname;
1875 DynStr artfrommail;
1876 DynStr artsubj;
1877 DynStr arttime;
1878 int articleTextTopLine = 0;
1879 int articleDestTextTopLine = 0;
1880 //__gshared Timer unreadTimer; // as main pane is never destroyed, there's no need to kill the timer
1881 //__gshared Folder unreadFolder;
1882 //__gshared uint unreadIdx;
1883 int linesInHeader;
1884 // folder view
1885 DynStr folderTopTag = null;
1886 DynStr folderCurrTag = null;
1887 int folderTopIndex = 0;
1888 int folderCurrIndex = 0;
1889 // message list view
1890 uint msglistTopUId = 0;
1891 uint msglistCurrUId = 0;
1892 bool saveMsgListPositions = false;
1893 MonoTime lastStateSaveTime;
1894 uint viewDataVersion;
1896 this () {
1897 super(null, GxSize(screenWidth, screenHeight));
1898 allowMinimise = false;
1899 allowDragMove = false;
1900 mType = Type.OnBottom;
1901 add();
1902 loadSavedState();
1903 lastStateSaveTime = MonoTime.currTime;
1904 viewDataVersion = dbView.getDataVersion()-1u;
1907 bool isViewChanged () const nothrow @trusted @nogc {
1908 pragma(inline, true);
1909 return (viewDataVersion != dbView.getDataVersion());
1912 void updateViewsIfTid (uint tid) {
1913 if (tid && lastDecodedTid == tid) {
1914 switchToFolderTid(lastDecodedTid, forced:true);
1918 void checkSaveState () {
1919 if (lastStateSaveTime+dur!"seconds"(30) <= MonoTime.currTime) {
1920 //writeln("saving state...");
1921 saveCurrentState();
1922 lastStateSaveTime = MonoTime.currTime;
1923 if (lastDecodedTid) {
1924 immutable maxuid = chiroGetTreePaneTableMaxUId();
1925 if (maxuid && maxuid > lastMaxTidUid) {
1926 // something was changed in the database, rescal
1927 updateViewsIfTid(lastDecodedTid);
1933 void saveCurrentState () {
1934 transacted!"Conf"{
1935 chiroSetOption("/mainpane/folders/toptid", folderTopTag);
1936 chiroSetOption("/mainpane/folders/currtid", folderCurrTag);
1937 saveThreadListPositions(transaction:false);
1941 void loadSavedState () {
1942 chiroGetOption(folderTopTag, "/mainpane/folders/toptid");
1943 chiroGetOption(folderCurrTag, "/mainpane/folders/currtid");
1944 //conwriteln("curr: ", folderCurrTag);
1945 //conwriteln(" top: ", folderTopTag);
1946 rescanFolders(forced:true);
1950 private void threadListPositionDirty () nothrow @safe @nogc {
1951 saveMsgListPositions = true;
1954 private void saveThreadListPositionsInternal () {
1955 if (lastDecodedTagName.length == 0) return;
1956 import core.stdc.stdio : snprintf;
1957 const(char)[] tn = lastDecodedTagName.getData;
1958 char[128] xname = void;
1959 auto xlen = snprintf(xname.ptr, xname.sizeof, "/mainpane/msgview/current%.*s", cast(uint)tn.length, tn.ptr);
1960 chiroSetOptionUInts(xname[0..xlen], msglistTopUId, msglistCurrUId);
1963 private void saveThreadListPositions (immutable bool transaction=true) {
1964 if (!saveMsgListPositions) return;
1965 saveMsgListPositions = false;
1966 if (lastDecodedTid == 0) return;
1967 if (transaction) {
1968 transacted!"Conf"(&saveThreadListPositionsInternal);
1969 } else {
1970 saveThreadListPositionsInternal();
1974 private void loadThreadListPositions () {
1975 saveMsgListPositions = false;
1976 msglistTopUId = msglistCurrUId = 0;
1977 if (lastDecodedTid == 0) return;
1978 import core.stdc.stdio : snprintf;
1979 char[128] xname = void;
1980 const(char)[] tn = lastDecodedTagName.getData;
1981 auto xlen = snprintf(xname.ptr, xname.sizeof, "/mainpane/msgview/current%.*s", cast(uint)tn.length, tn.ptr);
1982 chiroGetOptionUInts(ref msglistTopUId, ref msglistCurrUId, xname[0..xlen]);
1983 setupUnreadTimer();
1987 private int getFolderMonthLimit () {
1988 version(none) {
1989 import core.stdc.stdio : snprintf;
1990 char[1024] xname = void;
1991 if (lastDecodedTid == 0) return chiroGetOption!int("/mainpane/msgview/monthlimit", 6);
1992 bool exists;
1993 const(char)[] tn = lastDecodedTagName.getData;
1994 for (;;) {
1995 auto len = snprintf(xname.ptr, xname.sizeof, "/mainpane/msgview/monthlimit%.*s", cast(uint)tn.length, tn.ptr);
1996 int v = chiroGetOptionEx!int(xname[0..len], out exists);
1997 //writeln(tn, " :: ", v, " :: ", exists);
1998 if (exists) return v;
1999 auto slp = tn.lastIndexOf('/', 1);
2000 if (slp <= 0) break;
2001 tn = tn[0..slp];
2003 return chiroGetOption!int("/mainpane/msgview/monthlimit", 6);
2004 } else {
2005 if (lastDecodedTid == 0) return chiroGetOption!int("/mainpane/msgview/monthlimit", 6);
2006 return chiroGetTagMonthLimit(lastDecodedTagName.getData, 6);
2011 void rescanFolders (bool forced=false) {
2012 if (!.rescanFolders()) { if (!forced) return; }
2013 folderTopIndex = folderCurrIndex = 0;
2014 bool foundTop = false, foundCurr = false;
2015 foreach (immutable idx, const FolderInfo fi; folderList) {
2016 if (!foundTop && fi.name == folderTopTag) { foundTop = true; folderTopIndex = cast(int)idx; }
2017 if (!foundCurr && fi.name == folderCurrTag) { foundCurr = true; folderCurrIndex = cast(int)idx; }
2019 if (!foundTop) folderTopTag.clear();
2020 if (!foundCurr) folderCurrTag.clear();
2023 void normTopIndex () {
2024 if (folderList.length) {
2025 if (folderTopIndex >= folderList.length) folderTopIndex = cast(uint)folderList.length-1;
2026 int hgt = screenHeight/(gxTextHeightUtf+2)-1;
2027 if (hgt < 1) hgt = 1;
2028 if (folderList.length > hgt && folderTopIndex > folderList.length-hgt-1) {
2029 folderTopIndex = folderList.length-hgt-1;
2031 if (folderTopTag != folderList[folderTopIndex].name) folderTopTag = folderList[folderTopIndex].name;
2035 bool folderScrollUp () {
2036 int oldidx = folderTopIndex;
2037 if (folderTopIndex > 0) {
2038 --folderTopIndex;
2039 normTopIndex();
2041 return (folderTopIndex != oldidx);
2044 bool folderScrollDown () {
2045 int oldidx = folderTopIndex;
2046 if (folderList.length > 1) {
2047 ++folderTopIndex;
2048 normTopIndex();
2050 return (folderTopIndex != oldidx);
2053 void folderMakeCurVisible () {
2054 rescanFolders();
2055 if (folderList.length) {
2056 if (folderCurrIndex >= folderList.length) folderCurrIndex = cast(uint)folderList.length-1;
2057 if (folderTopIndex >= folderList.length) folderTopIndex = cast(uint)folderList.length-1;
2058 if (folderCurrIndex-3 < folderTopIndex) {
2059 folderTopIndex = folderCurrIndex-3;
2060 if (folderTopIndex < 0) folderTopIndex = 0;
2062 int hgt = screenHeight/(gxTextHeightUtf+2)-1;
2063 if (hgt < 1) hgt = 1;
2064 if (folderCurrIndex+2 > folderTopIndex+hgt) {
2065 folderTopIndex = (folderCurrIndex+2 > hgt ? folderCurrIndex+2-hgt : 0);
2066 if (folderTopIndex < 0) folderTopIndex = 0;
2068 if (folderTopTag != folderList[folderTopIndex].name) folderTopTag = folderList[folderTopIndex].name;
2069 if (folderCurrTag != folderList[folderCurrIndex].name) folderCurrTag = folderList[folderCurrIndex].name;
2070 } else {
2071 folderCurrIndex = folderTopIndex = 0;
2075 private void purgeMessages (const uint tid) {
2076 import core.stdc.stdio : snprintf;
2077 char[128] xname = void;
2078 int xlen;
2079 if (tid != 0) {
2080 auto tname = chiroGetTagName(tid);
2081 const(char)[] tn = tname.getData;
2082 xlen = cast(int)snprintf(xname.ptr, xname.sizeof, "/mainpane/msgview/current%.*s",
2083 cast(uint)tn.length, tn.ptr);
2084 // also, move down if deleted
2085 uint topUid = 0, currUid = 0;
2086 // get positions
2087 if (tid == lastDecodedTid) {
2088 topUid = msglistTopUId;
2089 currUid = msglistCurrUId;
2090 } else {
2091 chiroGetOptionUInts(ref topUid, ref currUid, xname[0..xlen]);
2093 // move
2094 immutable uint origmsgid = currUid;
2095 int pass = 0;
2096 do {
2097 immutable int app = chiroGetMessageAppearance(tid, currUid);
2098 if (app == Appearance.SoftDeletePurge) {
2099 immutable uint muid =
2100 (pass ? chiroGetTreePaneTableNextUid(currUid) // pass 1: down
2101 : chiroGetTreePaneTablePrevUid(currUid)); // pass 0: up
2102 if (!muid || muid == currUid) { // oops
2103 // restore position
2104 currUid = origmsgid;
2105 // change direction
2106 pass += 1;
2107 } else {
2108 currUid = muid;
2110 } else {
2111 pass = 3;
2113 } while (pass < 2);
2114 //conwriteln("orig: ", origmsgid, "; new: ", currUid, "; xname:", xname[0..xlen]);
2115 // purge
2116 chiroDeletePurgedWithTag(tid);
2117 // save positions
2118 if (tid == lastDecodedTid) {
2119 msglistTopUId = topUid;
2120 msglistCurrUId = currUid;
2122 // save new position
2123 if (tid == lastDecodedTid) {
2124 threadListPositionDirty();
2125 } else if (currUid != origmsgid) {
2126 transacted!"Conf"(() {
2127 chiroSetOptionUInts(xname[0..xlen], topUid, currUid);
2133 private void switchToFolderTid (const uint tid, bool forced=false) {
2134 //setupTrayAnimation();
2135 if (!forced && tid == lastDecodedTid) return;
2136 if (!forced) {
2137 purgeMessages(lastDecodedTid);
2138 if (tid != lastDecodedTid) purgeMessages(tid);
2140 saveThreadListPositions();
2141 // rescan
2142 lastDecodedTid = tid;
2143 if (tid != 0) {
2144 lastDecodedTagName = chiroGetTagName(tid);
2145 immutable int monthlimit = getFolderMonthLimit();
2146 chiroCreateTreePaneTable(tid, lastmonthes:monthlimit);
2147 // load position
2148 loadThreadListPositions();
2149 // top
2150 if (!chiroIsTreePaneTableUidValid(msglistTopUId)) {
2151 msglistTopUId = chiroGetTreePaneTableIndex2Uid(0);
2152 threadListPositionDirty();
2154 // current
2155 if (!chiroIsTreePaneTableUidValid(msglistCurrUId)) {
2156 msglistCurrUId = chiroGetTreePaneTableIndex2Uid(0);
2157 // find first unread, or position to the last
2158 if (!chiroGetMessageUnread(lastDecodedTid, msglistCurrUId)) {
2159 uint xid = chiroGetPaneNextUnread(msglistCurrUId);
2160 if (xid != 0) {
2161 msglistCurrUId = chiroGetTreePaneTablePrevUid(xid);
2162 } else {
2163 threadListEnd();
2166 threadListPositionDirty();
2168 lastMaxTidUid = chiroGetTreePaneTableMaxUId();
2169 setupUnreadTimer();
2170 } else {
2171 lastDecodedTagName.clear();
2172 msglistTopUId = 0;
2173 msglistCurrUId = 0;
2174 chiroClearTreePaneTable();
2178 void folderSetToIndex (int idx) {
2179 if (idx < 0 || idx >= folderList.length) return;
2180 if (idx == folderCurrIndex) return;
2181 folderCurrIndex = idx;
2182 folderCurrTag = folderList[folderCurrIndex].name;
2183 switchToFolderTid(folderList[folderCurrIndex].tagid);
2186 bool folderUpOne () {
2187 if (folderList.length == 0) return false;
2188 if (folderCurrIndex <= 0) return false;
2189 --folderCurrIndex;
2190 folderCurrTag = folderList[folderCurrIndex].name;
2191 setupUnreadTimer();
2192 return true;
2195 bool folderDownOne () {
2196 if (folderList.length == 0) return false;
2197 if (folderCurrIndex+1 >= cast(int)folderList.length) return false;
2198 ++folderCurrIndex;
2199 folderCurrTag = folderList[folderCurrIndex].name;
2200 setupUnreadTimer();
2201 return true;
2204 bool threadListHome () {
2205 if (lastDecodedTid == 0) return false;
2206 immutable uint firstUid = chiroGetTreePaneTableFirstUid();
2207 if (!firstUid || firstUid == msglistCurrUId) return false;
2208 msglistCurrUId = firstUid;
2209 threadListPositionDirty();
2210 setupUnreadTimer();
2211 return true;
2214 bool threadListEnd () {
2215 if (lastDecodedTid == 0) return false;
2216 immutable uint lastUid = chiroGetTreePaneTableLastUid();
2217 if (!lastUid || lastUid == msglistCurrUId) return false;
2218 msglistCurrUId = lastUid;
2219 threadListPositionDirty();
2220 setupUnreadTimer();
2221 return true;
2224 bool threadListUp () {
2225 import iv.timer;
2226 if (lastDecodedTid == 0) return false;
2227 auto ctm = Timer(true);
2228 immutable uint prevUid = chiroGetTreePaneTablePrevUid(msglistCurrUId);
2229 ctm.stop;
2230 if (ChiroTimerExEnabled) writeln("threadListUp time: ", ctm);
2231 if (!prevUid || prevUid == msglistCurrUId) return false;
2232 msglistCurrUId = prevUid;
2233 threadListPositionDirty();
2234 setupUnreadTimer();
2235 return true;
2238 bool threadListDown () {
2239 import iv.timer;
2240 if (lastDecodedTid == 0) return false;
2241 auto ctm = Timer(true);
2242 immutable uint nextUid = chiroGetTreePaneTableNextUid(msglistCurrUId);
2243 ctm.stop;
2244 if (ChiroTimerExEnabled) writeln("threadListDown time: ", ctm);
2245 if (!nextUid || nextUid == msglistCurrUId) return false;
2246 msglistCurrUId = nextUid;
2247 threadListPositionDirty();
2248 setupUnreadTimer();
2249 return true;
2252 bool threadListScrollUp (bool movecurrent) {
2253 import iv.timer;
2254 if (lastDecodedTid == 0) return false;
2255 auto ctm = Timer(true);
2256 immutable uint topPrevUid = chiroGetTreePaneTablePrevUid(msglistTopUId);
2257 ctm.stop;
2258 if (ChiroTimerExEnabled) conwriteln("threadListScrollUp: prevtop time: ", ctm);
2259 if (!topPrevUid || topPrevUid == msglistTopUId) return false;
2260 ctm.restart;
2261 immutable uint currPrevUid = (movecurrent ? chiroGetTreePaneTablePrevUid(msglistCurrUId) : 0);
2262 ctm.stop;
2263 if (movecurrent && ChiroTimerExEnabled) conwriteln("threadListScrollUp: prevcurr time: ", ctm);
2264 if (movecurrent && !currPrevUid) return false;
2265 msglistTopUId = topPrevUid;
2266 if (movecurrent) {
2267 msglistCurrUId = currPrevUid;
2268 setupUnreadTimer();
2270 threadListPositionDirty();
2271 return true;
2274 bool threadListScrollDown (bool movecurrent) {
2275 import iv.timer;
2276 if (lastDecodedTid == 0) return false;
2277 auto ctm = Timer(true);
2278 immutable uint currNextUid = (movecurrent ? chiroGetTreePaneTableNextUid(msglistCurrUId) : 0);
2279 ctm.stop;
2280 if (movecurrent && ChiroTimerExEnabled) writeln("threadListScrollDown: nextcurr time: ", ctm);
2281 if (movecurrent && (!currNextUid || currNextUid == msglistCurrUId)) return false;
2282 ctm.restart;
2283 immutable uint topNextUid = chiroGetTreePaneTableNextUid(msglistTopUId);
2284 ctm.stop;
2285 if (ChiroTimerExEnabled) writeln("threadListScrollDown: nexttop time: ", ctm);
2286 if (!topNextUid) return false;
2287 msglistTopUId = topNextUid;
2288 if (movecurrent) {
2289 msglistCurrUId = currNextUid;
2290 setupUnreadTimer();
2292 threadListPositionDirty();
2293 return true;
2296 bool threadListPageUp () {
2297 import iv.timer;
2298 bool res = false;
2299 int hgt = guiThreadListHeight/gxTextHeightUtf-1;
2300 if (hgt < 1) hgt = 1;
2301 auto ctm = Timer(true);
2302 foreach (; 0..hgt) {
2303 if (!threadListScrollUp(movecurrent:true)) break;
2304 res = true;
2306 ctm.stop;
2307 if (ChiroTimerExEnabled) writeln("threadListPageUp time: ", ctm);
2308 if (res) setupUnreadTimer();
2309 return res;
2312 bool threadListPageDown () {
2313 import iv.timer;
2314 bool res = false;
2315 int hgt = guiThreadListHeight/gxTextHeightUtf-1;
2316 if (hgt < 1) hgt = 1;
2317 auto ctm = Timer(true);
2318 foreach (; 0..hgt) {
2319 if (!threadListScrollDown(movecurrent:true)) break;
2320 res = true;
2322 ctm.stop;
2323 if (ChiroTimerExEnabled) writeln("threadListPageDown time: ", ctm);
2324 if (res) setupUnreadTimer();
2325 return res;
2329 // //////////////////////////////////////////////////////////////////// //
2330 static struct WebLink {
2331 int ly; // in lines
2332 int x, len; // in pixels
2333 dynstring url;
2334 dynstring text; // visual text
2335 int attachnum = -1;
2336 bool nofirst = false;
2338 @property bool isAttach () const pure nothrow @safe @nogc { return (attachnum >= 0); }
2341 WebLink[] emurls;
2342 int lastUrlIndex = -1;
2344 void appendUrl (in ref WebLink wl) {
2345 immutable oldcap = emurls.capacity;
2346 if (oldcap && emurls.length < oldcap) {
2347 import core.stdc.string : memset;
2348 memset(emurls.ptr+emurls.length, 0, (oldcap-emurls.length)*emurls[0].sizeof);
2350 emurls ~= WebLink();
2351 if (emurls.capacity > oldcap) {
2352 import core.stdc.string : memset;
2353 memset(emurls.ptr+oldcap, 0, (emurls.capacity-oldcap)*emurls[0].sizeof);
2355 emurls[$-1] = wl;
2358 void appendEmLine () {
2359 immutable oldcap = emlines.capacity;
2360 if (oldcap && emlines.length < oldcap) {
2361 import core.stdc.string : memset;
2362 memset(emlines.ptr+emlines.length, 0, (oldcap-emlines.length)*emlines[0].sizeof);
2364 emlines ~= dynstring();
2365 if (emlines.capacity > oldcap) {
2366 import core.stdc.string : memset;
2367 memset(emlines.ptr+oldcap, 0, (emlines.capacity-oldcap)*emlines[0].sizeof);
2371 void appendEmLine (in ref dynstring s) {
2372 appendEmLine();
2373 emlines[$-1] = s;
2376 void appendEmLine (const(char)[] buf) {
2377 appendEmLine();
2378 emlines[$-1] = dynstring(buf);
2381 void clearDecodedText () {
2382 import core.stdc.string : memset;
2383 if (emlines.length) {
2384 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "000: *** clearing %u emlines (%u) ***\n", cast(uint)emlines.length, cast(uint)emlines.capacity); }
2385 foreach (ref dynstring s; emlines) s.clear();
2386 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "001: *** clearing %u emlines (%u) ***\n", cast(uint)emlines.length, cast(uint)emlines.capacity); }
2387 immutable maxsz = (emlines.capacity > emlines.length ? emlines.capacity : emlines.length);
2388 memset(emlines.ptr, 0, maxsz*emlines[0].sizeof);
2389 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "002: *** clearing %u emlines (%u) ***\n", cast(uint)emlines.length, cast(uint)emlines.capacity); }
2390 emlines.length = 0;
2391 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "003: *** clearing %u emlines (%u) ***\n", cast(uint)emlines.length, cast(uint)emlines.capacity); }
2392 //!if (emlines.capacity) emlines.assumeSafeAppend;
2393 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "004: *** clearing %u emlines (%u) ***\n", cast(uint)emlines.length, cast(uint)emlines.capacity); }
2394 if (emlines.capacity) memset(emlines.ptr, 0, emlines.capacity*emlines[0].sizeof);
2395 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "005: *** clearing %u emlines (%u) ***\n", cast(uint)emlines.length, cast(uint)emlines.capacity); }
2397 if (emurls.capacity || emurls.length) {
2398 foreach (ref WebLink l; emurls) { l.url.clear(); l.text.clear(); }
2399 immutable maxsz = (emurls.capacity > emurls.length ? emurls.capacity : emurls.length);
2400 memset(emurls.ptr, 0, maxsz*emurls[0].sizeof);
2401 emurls.length = 0;
2402 //!if (emurls.capacity) emurls.assumeSafeAppend;
2403 if (emurls.capacity) memset(emurls.ptr, 0, emurls.capacity*emurls[0].sizeof);
2405 lastDecodedUId = 0;
2406 articleTextTopLine = 0;
2407 articleDestTextTopLine = 0;
2408 lastUrlIndex = -1;
2409 arttoname.clear();
2410 arttomail.clear();
2411 artfromname.clear();
2412 artfrommail.clear();
2413 artsubj.clear();
2414 arttime.clear();
2417 // <0: not on url
2418 int findUrlIndexAt (int mx, int my) {
2419 int tpX0 = guiGroupListWidth+2+1;
2420 int tpX1 = screenWidth-1-guiMessageTextLPad*2-guiScrollbarWidth;
2421 int tpY0 = guiThreadListHeight+1;
2422 int tpY1 = screenHeight-1;
2424 int y = tpY0+linesInHeader*gxTextHeightUtf+2+1+guiMessageTextVPad;
2426 if (mx < tpX0 || mx > tpX1) return -1;
2427 if (my < y || my > tpY1) return -1;
2429 mx -= tpX0;
2431 // yeah, i can easily calculate this, i know
2432 uint idx = articleTextTopLine;
2433 while (idx < emlines.length && y < screenHeight) {
2434 if (my >= y && my < y+gxTextHeightUtf) {
2435 foreach (immutable uidx, const ref WebLink wl; emurls) {
2436 //conwriteln("checking url [#", uidx, "]; idx=", idx, "; ly=", wl.ly);
2437 if (wl.ly == idx) {
2438 if (mx >= wl.x && mx < wl.x+wl.len) return cast(int)uidx;
2442 ++idx;
2443 y += gxTextHeightUtf+guiMessageTextInterline;
2446 return -1;
2449 WebLink* findUrlAt (int mx, int my) {
2450 auto uidx = findUrlIndexAt(mx, my);
2451 return (uidx >= 0 ? &emurls[uidx] : null);
2454 private void emlDetectUrls (uint textlines) {
2455 lastUrlIndex = -1;
2458 if (textlines > emlines.length) textlines = cast(uint)emlines.length; // just in case
2459 foreach (immutable cy, dynstring s; emlines[0..textlines]) {
2460 if (s.length == 0) continue;
2461 detectUrl(s.getData(), 0, (const(char)[] url, usize spos, usize epos) {
2462 WebLink wl;
2463 wl.nofirst = (spos > 0);
2464 wl.ly = cast(int)cy;
2465 auto kr = GxKerning(4, 0);
2466 s[0..epos].utfByDCharSPos(delegate (dchar ch, usize stpos) {
2467 int w = kr.fixWidthPre(ch);
2468 if (stpos == spos) wl.x = w;
2470 wl.len = kr.finalWidth-wl.x;
2471 wl.url = wl.text = s[spos..epos];
2472 for (;;) {
2473 auto pp = wl.url.indexOf("..");
2474 if (pp < 0) break;
2475 wl.url = wl.url[0..pp]~wl.url[pp+1..$];
2477 appendUrl(wl); //emurls ~= wl;
2478 return true; // go on
2482 int attachcount = 0;
2483 foreach (immutable uint cy; textlines..cast(uint)emlines.length) {
2484 dynstring s = emlines[cy];
2485 if (s.length == 0) continue;
2486 auto spos = s.indexOf("attach:");
2487 if (spos < 0) continue;
2488 auto epos = spos+7;
2489 while (epos < s.length && s.ptr[epos] > ' ') ++epos;
2490 //if (attachcount >= parts.length) break;
2491 WebLink wl;
2492 wl.nofirst = (spos > 0);
2493 wl.ly = cast(int)cy;
2494 //wl.x = gxTextWidthUtf(s[0..spos]);
2495 //wl.len = gxTextWidthUtf(s[spos..epos]);
2496 auto kr = GxKerning(4, 0);
2497 s[0..epos].utfByDCharSPos(delegate (dchar ch, usize stpos) {
2498 int w = kr.fixWidthPre(ch);
2499 if (stpos == spos) wl.x = w;
2501 wl.len = kr.finalWidth-wl.x;
2502 wl.url = wl.text = s[spos..epos];
2503 //if (spos > 0) ++wl.x; // this makes text bolder, no need to
2504 wl.attachnum = attachcount;
2505 //wl.attachfname = s[spos+7..epos];
2506 //wl.part = parts[attachcount];
2507 ++attachcount;
2508 appendUrl(wl); //emurls ~= wl;
2512 bool needToDecodeArticleTextNL (uint uid) const nothrow @trusted @nogc {
2513 return (lastDecodedUId != uid);
2516 // fld is locked here
2517 void decodeArticleTextNL (uint uid) {
2518 static auto stmtGet = LazyStatement!"View"(`
2519 SELECT
2520 from_name AS fromName
2521 , from_mail AS fromMail
2522 , to_name AS toName
2523 , to_mail AS toMail
2524 , subj AS subj
2525 , datetime(threads.time, 'unixepoch') AS time
2526 , ChiroUnpack(content_text.content) AS text
2527 , ChiroUnpack(content_html.content) AS html
2528 FROM info
2529 INNER JOIN threads USING(uid)
2530 INNER JOIN content_text USING(uid)
2531 INNER JOIN content_html USING(uid)
2532 WHERE uid=:uid
2533 LIMIT 1
2534 ;`);
2536 if (!needToDecodeArticleTextNL(uid)) return;
2538 //if (uid == 0) { clearDecodedText(); return; }
2539 //conwriteln("200: em.len=", emlines.length);
2540 clearDecodedText();
2541 //conwriteln("201: em.len=", emlines.length);
2542 lastDecodedUId = uid;
2544 // get article content
2545 //FIXME: see `art.getTextContent()` for GPG decryption!
2546 DynStr artcontent;
2547 bool ishtml = false;
2548 bool htmlheader = false;
2549 foreach (auto row; stmtGet.st.bind(":uid", uid).range) {
2550 auto text = row.text!SQ3Text;
2551 auto html = row.html!SQ3Text;
2552 bool isHtml = ((text.xstrip.length == 0 || preferHtmlContent) && html.xstrip.length != 0);
2553 bool forceHtml = false;
2554 if (detectHtmlContent && !isHtml && html.xstrip.length == 0 && text.xstrip.length != 0) {
2555 auto tmp = text.xstrip;
2556 if (tmp.startsWithCI("<!DOCTYPE")) forceHtml = true;
2558 //conwriteln("! ", text.xstrip.length, " ! ", html.xstrip.length);
2559 if (isHtml || forceHtml) {
2560 version(article_can_into_html) {
2561 try {
2562 string s = htmlToText((forceHtml ? text.idup : html.idup), false);
2563 //conwriteln("000: ac.len=", artcontent.length);
2564 artcontent ~= s;
2565 //conwriteln("001: ac.len=", artcontent.length);
2566 artcontent.removeASCIICtrls();
2567 //conwriteln("002: ac.len=", artcontent.length);
2568 htmlheader = true;
2569 } catch (Exception e) {
2570 artcontent ~= (forceHtml ? text : html);
2572 } else {
2573 artcontent ~= (forceHtml ? text : html);
2575 ishtml = true;
2576 } else if (text.length != 0) {
2577 artcontent ~= text;
2578 } else {
2579 artcontent ~= "no text content";
2581 arttoname = row.toName!SQ3Text;
2582 arttomail = row.toMail!SQ3Text;
2583 artfromname = row.fromName!SQ3Text;
2584 artfrommail = row.fromMail!SQ3Text;
2585 artsubj = row.subj!SQ3Text;
2586 arttime = row.time!SQ3Text;
2587 if (artsubj.length == 0) artsubj = "no subject";
2590 if (artcontent.length == 0) return; // no text
2591 //conwriteln("100: ac.len=", artcontent.length, "; em.len=", emlines.length);
2592 appendEmLine(); //emlines ~= dynstring(); // hack; this dummy line will be removed
2593 //conwriteln("101: ac.len=", artcontent.length, "; em.len=", emlines.length);
2594 bool skipEmptyLines = true;
2596 if (htmlheader) {
2597 appendEmLine("\x01==============================================");
2598 appendEmLine("\x01--- HTML CONTENT ---");
2599 appendEmLine("\x01==============================================");
2600 //emlines ~= null;
2603 bool lastEndsWithSpace () { return (emlines[$-1].length ? emlines[$-1][$-1] == ' ' : false); }
2605 int lastQLevel = 0;
2607 static dynstring addQuotes (dynstring s, int qlevel) {
2608 enum QuoteStr = ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ";
2609 if (qlevel <= 0) return s;
2610 if (qlevel > QuoteStr.length-1) qlevel = cast(int)QuoteStr.length-1;
2611 return dynstring(QuoteStr[$-qlevel-1..$])~s;
2614 // returns quote level
2615 static int removeQuoting (ref ConString s) {
2616 // calculate quote level
2617 int qlevel = 0;
2618 if (s.length && s[0] == '>') {
2619 usize lastqpos = 0, pos = 0;
2620 while (pos < s.length) {
2621 if (s.ptr[pos] != '>') {
2622 if (s.ptr[pos] != ' ') break;
2623 } else {
2624 lastqpos = pos;
2625 ++qlevel;
2627 ++pos;
2629 if (s.length-lastqpos > 1 && s.ptr[lastqpos+1] == ' ') ++lastqpos;
2630 ++lastqpos;
2631 s = s[lastqpos..$];
2633 return qlevel;
2636 bool inCode = false;
2638 void putLine (ConString s) {
2639 int qlevel = (ishtml ? 0 : removeQuoting(s));
2640 //conwriteln("qlevel=", qlevel, "; s=[", s, "]");
2641 // empty line: just insert it
2642 if (s.length == 0) {
2643 if (skipEmptyLines) return;
2644 if (qlevel == 0 && emlines[$-1].xstripright.length == 0) return;
2645 appendEmLine(addQuotes(dynstring(), qlevel).xstripright);
2646 } else {
2647 if (s.xstrip.length == 0) {
2648 if (skipEmptyLines) return;
2649 if (qlevel == 0 && emlines[$-1].xstripright.length == 0) return;
2651 skipEmptyLines = false;
2652 // join code lines if it is possible
2653 if (inCode && qlevel == lastQLevel && lastEndsWithSpace) {
2654 //conwriteln("<", s, ">");
2655 emlines[$-1] ~= addQuotes(dynstring(s.xstrip), qlevel);
2656 return;
2658 // two spaces at the beginning usually means "this is code"; don't wrap it
2659 if (s.length >= 1 && s[0] == '\t') {
2660 appendEmLine(addQuotes(dynstring(s), qlevel));
2661 // join next lines if it is possible
2662 inCode = true;
2663 //conwriteln("[", s, "]");
2664 lastQLevel = qlevel;
2665 return;
2667 inCode = false;
2668 // can we append?
2669 bool newline = false;
2670 if (lastQLevel != qlevel || !lastEndsWithSpace) {
2671 newline = true;
2672 } else {
2673 // append words
2674 //while (s.length > 1 && s.ptr[0] <= ' ') s = s[1..$];
2676 while (s.length) {
2677 usize epos = 0;
2678 if (newline) {
2679 while (epos < s.length && s.ptr[epos] <= ' ') ++epos;
2680 } else {
2681 //assert(s[0] > ' ');
2682 while (epos < s.length && s.ptr[epos] <= ' ') ++epos;
2684 while (epos < s.length && s.ptr[epos] > ' ') ++epos;
2685 auto xlen = epos;
2686 while (epos < s.length && s.ptr[epos] <= ' ') ++epos;
2687 if (!newline && emlines[$-1].length+xlen <= 80) {
2688 // no wrapping, continue last line
2689 emlines[$-1] ~= s[0..epos];
2690 } else {
2691 newline = false;
2692 // wrapping; new line
2693 appendEmLine(addQuotes(dynstring(s[0..epos]), qlevel));
2695 s = s[epos..$];
2697 if (newline) appendEmLine(addQuotes(dynstring(), qlevel));
2699 lastQLevel = qlevel;
2702 try {
2703 //foreach (ConString s; LineIterator!false(art.getTextContent)) putLine(s);
2704 const(char)[] buf = artcontent;
2705 while (buf.length) {
2706 usize epos = skipOneLine(buf, 0);
2707 usize eend = epos;
2708 if (eend >= 2 && buf[eend-2] == '\r' && buf[eend-1] == '\n') eend -= 2;
2709 else if (eend >= 1 && buf[eend-1] == '\n') eend -= 1;
2710 putLine(buf[0..eend]);
2711 buf = buf[epos..$];
2713 // remove first dummy line
2714 if (emlines.length) emlines = emlines[1..$];
2715 // remove trailing empty lines
2716 while (emlines.length && emlines[$-1].xstrip.length == 0) emlines.length -= 1;
2717 } catch (Exception e) {
2718 conwriteln("================================= ERROR: ", e.msg, " =================================");
2719 conwriteln(e.toString);
2723 // attaches
2724 auto lcount = cast(uint)emlines.length;
2725 emlinesBeforeAttaches = lcount;
2727 uint attcount = 0;
2728 static auto statAttaches = LazyStatement!"View"(`
2729 SELECT
2730 idx AS idx
2731 , mime AS mime
2732 , name AS name
2733 FROM attaches
2734 WHERE uid=:uid
2735 ORDER BY idx
2736 ;`);
2737 foreach (auto row; statAttaches.st.bind(":uid", uid).range) {
2738 import core.stdc.stdio : snprintf;
2739 if (attcount == 0) { appendEmLine(); appendEmLine(); }
2740 DynStr att;
2741 char[128] buf = void;
2742 //if (type.length == 0) type = "unknown/unknown";
2743 //string s = " [%02s] attach:%s -- %s".format(attcount, Article.fixAttachmentName(filename), type);
2744 auto mime = row.mime!SQ3Text;
2745 auto name = row.name!SQ3Text/*.decodeSubj*/;
2746 auto len = snprintf(buf.ptr, buf.sizeof, " [%2u] attach:%.*s -- %.*s",
2747 attcount, cast(uint)name.length, name.ptr, cast(uint)mime.length, mime.ptr);
2748 assert(len > 0);
2749 if (len > buf.sizeof) len = cast(int)buf.sizeof;
2750 appendEmLine(buf[0..len]);
2751 ++attcount;
2753 /*FIXME
2754 art.forEachAttachment(delegate(ConString type, ConString filename) {
2755 if (attcount == 0) { emlines ~= null; emlines ~= null; }
2756 import std.format : format;
2757 if (type.length == 0) type = "unknown/unknown";
2758 string s = " [%02s] attach:%s -- %s".format(attcount, Article.fixAttachmentName(filename), type);
2759 emlines ~= s;
2760 ++attcount;
2761 return false;
2764 emlDetectUrls(lcount);
2767 @property int visibleArticleLines () {
2768 int y = guiThreadListHeight+1+linesInHeader*gxTextHeightUtf+2+guiMessageTextVPad;
2769 return (screenHeight-y)/(gxTextHeightUtf+guiMessageTextInterline);
2772 void normalizeArticleTopLine () {
2773 int lines = visibleArticleLines;
2774 if (lines < 1 || emlines.length <= lines) {
2775 articleTextTopLine = 0;
2776 articleDestTextTopLine = 0;
2777 } else {
2778 if (articleTextTopLine < 0) articleTextTopLine = 0;
2779 if (articleTextTopLine+lines > emlines.length) {
2780 articleTextTopLine = cast(int)emlines.length-lines;
2781 if (articleTextTopLine < 0) articleTextTopLine = 0;
2786 void doScrollStep () {
2787 auto oldtop = articleTextTopLine;
2788 foreach (immutable _; 0..6) {
2789 normalizeArticleTopLine();
2790 if (articleDestTextTopLine < articleTextTopLine) {
2791 --articleTextTopLine;
2792 } else if (articleDestTextTopLine > articleTextTopLine) {
2793 ++articleTextTopLine;
2794 } else {
2795 break;
2797 normalizeArticleTopLine();
2799 if (articleTextTopLine == oldtop) {
2800 // can't scroll anymore
2801 articleDestTextTopLine = articleTextTopLine;
2802 return;
2804 postScreenRebuild();
2805 postArticleScroll();
2808 void scrollBy (int delta) {
2809 articleDestTextTopLine += delta;
2810 doScrollStep();
2813 void scrollByPageUp () {
2814 int lines = visibleArticleLines-scrollKeepLines;
2815 if (lines < 1) lines = 1;
2816 scrollBy(-lines);
2819 void scrollByPageDown () {
2820 int lines = visibleArticleLines-scrollKeepLines;
2821 if (lines < 1) lines = 1;
2822 scrollBy(lines);
2825 // ////////////////////////////////////////////////////////////////// //
2826 // this also fixes current/top uids
2827 void getAndFixThreadListIndicies (out int topidx, out int curridx) {
2828 int hgt = guiThreadListHeight/gxTextHeightUtf-1;
2829 if (hgt < 1) hgt = 1;
2831 topidx = chiroGetTreePaneTableUid2Index(msglistTopUId);
2832 immutable origTopIdx = topidx;
2833 if (topidx < 0) topidx = 0;
2834 curridx = chiroGetTreePaneTableUid2Index(msglistCurrUId);
2835 immutable origCurrIdx = curridx;
2836 if (curridx < 0) curridx = 0;
2838 //conwriteln("topuid: ", msglistTopUId, "; topidx=", topidx);
2839 //conwriteln("curruid: ", msglistCurrUId, "; curridx=", curridx);
2841 if (curridx-3 < topidx) {
2842 topidx = curridx-3;
2843 if (topidx < 0) topidx = 0;
2845 if (curridx+3 > topidx+hgt) {
2846 topidx = (curridx+3 > hgt ? curridx+3-hgt : 0);
2847 if (topidx < 0) topidx = 0;
2850 if (origCurrIdx != curridx || msglistCurrUId == 0) {
2851 immutable ocurr = msglistCurrUId;
2852 msglistCurrUId = chiroGetTreePaneTableIndex2Uid(curridx);
2853 if (ocurr != msglistCurrUId) {
2854 threadListPositionDirty();
2855 setupUnreadTimer();
2859 if (origTopIdx != topidx || msglistTopUId == 0) {
2860 immutable otop = msglistTopUId;
2861 msglistTopUId = chiroGetTreePaneTableIndex2Uid(topidx);
2862 if (otop != msglistTopUId) threadListPositionDirty();
2866 void setupUnreadTimer () {
2867 postMarkAsUnreadEvent();
2870 // //////////////////////////////////////////////////////////////////// //
2871 //TODO: move parts to widgets
2872 override void onPaint () {
2873 viewDataVersion = dbView.getDataVersion();
2874 gxClipReset();
2876 gxFillRect(0, 0, guiGroupListWidth, screenHeight, getColor("grouplist-back"));
2877 gxVLine(guiGroupListWidth, 0, screenHeight, getColor("grouplist-divline"));
2879 gxFillRect(guiGroupListWidth+1, 0, screenWidth, guiThreadListHeight, getColor("threadlist-back"));
2880 gxHLine(guiGroupListWidth+1, guiThreadListHeight, screenWidth, getColor("threadlist-divline"));
2882 // ////////////////////////////////////////////////////////////////// //
2883 void drawArticle (uint uid) {
2884 import core.stdc.stdio : snprintf;
2885 import std.format : format;
2886 import std.datetime;
2887 char[128] tbuf;
2888 const(char)[] tbufs;
2890 void xfmt (string s, const(char)[][] strs...) {
2891 int dpos = 0;
2892 void puts (const(char)[] s...) {
2893 foreach (char ch; s) {
2894 if (dpos >= tbuf.length) break;
2895 tbuf[dpos++] = ch;
2898 while (s.length) {
2899 if (strs.length && s.length > 1 && s[0] == '%' && s[1] == 's') {
2900 puts(strs[0]);
2901 strs = strs[1..$];
2902 s = s[2..$];
2903 } else {
2904 puts(s[0]);
2905 s = s[1..$];
2908 tbufs = tbuf[0..dpos];
2911 if (needToDecodeArticleTextNL(uid)) {
2912 decodeArticleTextNL(uid);
2916 gxClipX0 = guiGroupListWidth+2;
2917 gxClipX1 = screenWidth-1;
2918 gxClipY0 = guiThreadListHeight+1;
2919 gxClipY1 = screenHeight-1;
2921 gxClipRect = GxRect(GxPoint(guiGroupListWidth+2, guiThreadListHeight+1), GxPoint(screenWidth-1, screenHeight-1));
2923 int msx = lastMouseX;
2924 int msy = lastMouseY;
2926 int curDrawYMul = 1;
2928 // header
2929 immutable int hdrHeight = (3+(arttoname.length || arttomail.length ? 1 : 0))*gxTextHeightUtf+2;
2930 gxFillRect(gxClipRect.x0, gxClipRect.y0, gxClipRect.x1-gxClipRect.x0+1, hdrHeight, getColor("msg-header-back"));
2932 if (artfromname.length) {
2933 xfmt("From: %s <%s>", artfromname, artfrommail);
2934 } else {
2935 xfmt("From: %s", artfrommail);
2937 gxDrawTextUtf(gxClipRect.x0+guiMessageTextLPad, gxClipRect.y0+0*gxTextHeightUtf+1, tbufs, getColor("msg-header-from"));
2938 if (arttoname.length || arttomail.length) {
2939 if (arttoname.length) {
2940 xfmt("To: %s <%s>", arttoname, arttomail);
2941 } else {
2942 xfmt("To: %s", arttomail);
2944 gxDrawTextUtf(gxClipRect.x0+guiMessageTextLPad, gxClipRect.y0+curDrawYMul*gxTextHeightUtf+1, tbufs, getColor("msg-header-to"));
2945 ++curDrawYMul;
2947 xfmt("Subject: %s", (artsubj.length ? artsubj : "no subject"));
2948 gxDrawTextUtf(gxClipRect.x0+guiMessageTextLPad, gxClipRect.y0+curDrawYMul*gxTextHeightUtf+1, tbufs, getColor("msg-header-subj"));
2949 ++curDrawYMul;
2951 //auto t = SysTime.fromUnixTime(arttime);
2952 //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);
2953 auto tlen = snprintf(tbuf.ptr, tbuf.length, "Date: %.*s", cast(uint)arttime.length, arttime.ptr);
2954 gxDrawTextUtf(gxClipRect.x0+guiMessageTextLPad, gxClipRect.y0+curDrawYMul*gxTextHeightUtf+1, tbuf[0..tlen], getColor("msg-header-date"));
2955 ++curDrawYMul;
2959 // text
2960 linesInHeader = curDrawYMul;
2961 int y = gxClipRect.y0+curDrawYMul*gxTextHeightUtf+2;
2963 gxHLine(gxClipRect.x0, y, gxClipRect.x1-gxClipRect.x0+1, getColor("msg-header-divline"));
2964 ++y;
2965 gxFillRect(gxClipRect.x0, y, gxClipRect.x1-gxClipRect.x0+1, screenHeight-y, getColor("msg-text-back"));
2966 y += guiMessageTextVPad;
2968 immutable sty = y;
2970 normalizeArticleTopLine();
2972 bool drawUpMark = (articleTextTopLine > 0);
2973 bool drawDownMark = false;
2975 immutable uint messageTextNormalColor = getColor("msg-text-text");
2976 immutable uint messageTextQuote0Color = getColor("msg-text-quote0");
2977 immutable uint messageTextQuote1Color = getColor("msg-text-quote1");
2978 immutable uint messageTextHtmlHeaderColor = getColor("msg-text-html-sign");
2979 immutable uint messageTextLinkColor = getColor("msg-text-link");
2980 immutable uint messageTextLinkHoverColor = getColor("msg-text-link-hover");
2981 immutable uint messageTextLinkPressedColor = getColor("msg-text-link-pressed");
2983 uint idx = articleTextTopLine;
2984 bool msvisible = isMouseVisible;
2985 bool checkQuotes = true;
2986 if (emlines.length && emlines[0].length && emlines[0][0] == 1) {
2987 // html content
2988 checkQuotes = false;
2990 while (idx < emlines.length && y < screenHeight) {
2991 int qlevel = 0;
2992 dynstring s = emlines[idx];
2994 if (checkQuotes) {
2995 foreach (immutable char ch; s) {
2996 if (ch <= ' ') continue;
2997 if (ch != '>') break;
2998 ++qlevel;
3002 uint clr = messageTextNormalColor;
3003 if (qlevel) {
3004 final switch (qlevel%2) {
3005 case 0: clr = messageTextQuote0Color; break;
3006 case 1: clr = messageTextQuote1Color; break;
3010 if (!checkQuotes && s.length && s[0] == 1) {
3011 clr = messageTextHtmlHeaderColor;
3012 s = s[1..$];
3015 gxDrawTextUtfOpt(GxDrawTextOptions.TabColor(4, clr), gxClipRect.x0+guiMessageTextLPad, y, s);
3017 foreach (const ref WebLink wl; emurls) {
3018 if (wl.ly == idx) {
3019 uint lclr = messageTextLinkColor;
3020 if (msvisible && msy >= y && msy < y+gxTextHeightUtf &&
3021 msx >= gxClipRect.x0+1+guiMessageTextLPad+wl.x &&
3022 msx < gxClipRect.x0+1+guiMessageTextLPad+wl.x+wl.len)
3024 lclr = (lastMouseLeft ? messageTextLinkPressedColor : messageTextLinkHoverColor);
3025 //lclr = getColor("msg-text-link", (lastMouseLeft ? "pressed" : "hover"));
3027 gxDrawTextUtfOpt(GxDrawTextOptions.TabColorFirstFull(4, lclr, wl.nofirst), gxClipRect.x0+guiMessageTextLPad+wl.x, y, wl.text);
3031 if (gxClipRect.y1-y < gxTextHeightUtf && emlines.length-idx > 0) drawDownMark = true;
3033 ++idx;
3034 y += gxTextHeightUtf+guiMessageTextInterline;
3037 //gxDrawTextOutScaledUtf(1, gxClipRect.x1-gxTextWidthUtf(triangleDownStr)-3, sty, triangleDownStr, (drawUpMark ? gxRGB!(255, 255, 255) : gxRGB!(85, 85, 85)), gxRGB!(0, 69, 69));
3038 //gxDrawTextOutScaledUtf(1, gxClipRect.x1-gxTextWidthUtf(triangleUpStr)-3, gxClipRect.y1-7, triangleUpStr, (drawDownMark ? gxRGB!(255, 255, 255) : gxRGB!(85, 85, 85)), gxRGB!(0, 69, 69));
3040 gxDrawScrollBar(GxRect(gxClipRect.x1-10, sty+10, 5, gxClipRect.y1-sty-17), cast(int)emlines.length-1, idx-1);
3042 bool twited = false;
3043 DynStr twittext = chiroGetMessageTwit(lastDecodedTid, uid, out twited);
3044 if (twited) {
3045 immutable uint clr = getColor("twit-shade");
3047 foreach (immutable dy; gxClipRect.y0+3*gxTextHeightUtf+2..gxClipRect.y1+1) {
3048 foreach (immutable dx; gxClipRect.x0..gxClipRect.x1+1) {
3049 if ((dx^dy)&1) gxPutPixel(dx, dy, clr);
3053 GxRect rc = gxClipRect;
3054 rc.y0 = rc.y0+3*gxTextHeightUtf+2;
3055 gxDashRect(rc, clr);
3057 if (twittext.length) {
3058 immutable ofdz = egraFontSize;
3059 scope(exit) egraFontSize = ofdz;
3060 egraFontSize = 150;
3061 int tx = gxClipRect.x0+(gxClipRect.width-gxTextWidthUtf(twittext))/2-1;
3062 int ty = gxClipRect.y0+(gxClipRect.height-gxTextHeightUtf)/2-1;
3063 gxDrawTextOutUtf(tx, ty, twittext, getColor("twit-text"), getColor("twit-outline"));
3068 // ////////////////////////////////////////////////////////////////// //
3069 void drawThreadList () {
3070 uint tid = 0;
3071 if (folderCurrIndex >= 0 && folderCurrIndex < folderList.length) {
3072 tid = folderList[folderCurrIndex].tagid;
3074 switchToFolderTid(tid);
3076 // find indicies
3077 int topidx, curridx;
3078 getAndFixThreadListIndicies(out topidx, out curridx);
3080 int hgt = (guiThreadListHeight+gxTextHeightUtf-1)/gxTextHeightUtf;
3081 if (hgt < 1) hgt = 1;
3083 gxClipRect.x0 = guiGroupListWidth+2;
3084 gxClipRect.x1 = screenWidth-1-5;
3085 gxClipRect.y0 = 0;
3086 gxClipRect.y1 = guiThreadListHeight-1;
3087 immutable uint origX0 = gxClipRect.x0;
3088 immutable uint origX1 = gxClipRect.x1;
3089 immutable uint origY0 = gxClipRect.y0;
3090 immutable uint origY1 = gxClipRect.y1;
3091 int y = 0;
3093 static uint darkenBy (in uint clr, in int val) pure nothrow @safe @nogc {
3094 pragma(inline, true);
3095 return
3096 val ?
3097 (clr&0xff_00_00_00)|
3098 (cast(uint)clampToByte(cast(int)(cast(ubyte)(clr>>16))-val)<<16)|
3099 (cast(uint)clampToByte(cast(int)(cast(ubyte)(clr>>8))-val)<<8)|
3100 (cast(uint)clampToByte(cast(int)(cast(ubyte)clr)-val)) :
3101 clr;
3104 chiroGetPaneTablePage(topidx, hgt,
3105 delegate (int pgofs, /* offset from the page start, from zero and up to `limit` */
3106 int iid, /* item id, never zero */
3107 uint uid, /* msguid, never zero */
3108 uint parentuid, /* parent msguid, may be zero */
3109 uint level, /* threading level, from zero */
3110 Appearance appearance, /* see above */
3111 Mute mute, /* see above */
3112 SQ3Text date, /* string representation of receiving date and time */
3113 SQ3Text subj, /* message subject, can be empty string */
3114 SQ3Text fromName, /* message sender name, can be empty string */
3115 SQ3Text fromMail, /* message sender email, can be empty string */
3116 SQ3Text title) /* title from twiting */
3118 import std.format : format;
3119 import std.datetime;
3121 if (y >= guiThreadListHeight) return;
3122 if (subj.length == 0) subj = "no subject";
3124 gxClipRect.x0 = origX0;
3125 gxClipRect.x1 = origX1;
3127 int darken = (level != 0 && appearance != Appearance.Unread ? 40 : 0);
3129 string style = "normal";
3130 if (appearance == Appearance.Unread) {
3131 style = "unread";
3132 darken = 0;
3133 } else {
3134 if (mute > Mute.Normal) { style = "twit"; darken = 0; }
3136 if (isSoftDeleted(appearance)) {
3137 darken = 0;
3138 style = (appearance == Appearance.SoftDeletePurge ? "hard-del" : "soft-del");
3141 char[64] stfull = void;
3142 usize stpos = 0;
3143 if (uid == msglistCurrUId) {
3144 static immutable string sc = "cursor-";
3145 stpos = sc.length;
3146 stfull[0..stpos] = sc;
3148 stfull[stpos..stpos+style.length] = style;
3149 const(char)[] stname = stfull[0..stpos+style.length];
3151 char[128] stx = void;
3152 const(char)[] buildStyle (const(char)[] stt) {
3153 static immutable string sc = "threadlist-";
3154 stx[0..sc.length] = sc;
3155 usize xpos = sc.length;
3156 stx[xpos..xpos+stname.length] = stname;
3157 xpos += stname.length;
3158 stx[xpos++] = '-';
3159 stx[xpos..xpos+stt.length] = stt;
3160 xpos += stt.length;
3161 return stx[0..xpos];
3164 //conwriteln("STNAME: <", stname, ">; darken=", darken);
3166 immutable uint clrBack = getColor(buildStyle("back"));
3167 immutable uint clrOut = getColor(buildStyle("outline"));
3168 immutable uint clrFrom = darkenBy(getColor(buildStyle("from-text")), darken);
3169 immutable uint clrMail = darkenBy(getColor(buildStyle("mail-text")), darken);
3170 immutable uint clrSubj = darkenBy(getColor(buildStyle("subj-text")), darken);
3171 immutable uint clrTime = darkenBy(getColor(buildStyle("time-text")), darken);
3173 // background
3174 if (!gxIsTransparent(clrBack)) gxFillRect(gxClipRect.x0, y, gxClipRect.width-1, gxTextHeightUtf, clrBack);
3175 gxClipRect.x0 = gxClipRect.x0+1;
3176 gxClipRect.x1 = gxClipRect.x1-1;
3178 // time
3179 int timewdt = gxDrawTextOutUtf(gxClipRect.x1-gxTextWidthUtf(date), y, date, clrTime, clrOut);
3180 if (timewdt%8) timewdt = (timewdt|7)+1;
3182 SQ3Text from = fromName;
3184 auto vp = from.indexOf(" via Digitalmars-");
3185 if (vp > 0) {
3186 from = from[0..vp].xstrip;
3187 if (from.length == 0) from = "anonymous";
3191 // from/mail
3192 gxClipRect.x1 = gxClipRect.x1-/*(13*6+4)*2+33*/timewdt;
3193 enum FromWidth = 22*6*2+88;
3194 gxDrawTextOutUtf(gxClipRect.x1-FromWidth, y, from, clrFrom, clrOut);
3195 gxDrawTextOutUtf(gxClipRect.x1-FromWidth+gxTextWidthUtf(from)+4, y, "<", clrMail, clrOut);
3196 gxDrawTextOutUtf(gxClipRect.x1-FromWidth+gxTextWidthUtf(from)+4+gxTextWidthUtf("<")+1, y, fromMail, clrMail, clrOut);
3197 gxDrawTextOutUtf(gxClipRect.x1-FromWidth+gxTextWidthUtf(from)+4+gxTextWidthUtf("<")+1+gxTextWidthUtf(fromMail)+1, y, ">", clrMail, clrOut);
3198 gxClipRect.x1 = gxClipRect.x1-FromWidth-6;
3200 // subj
3201 gxDrawTextOutUtf(gxClipRect.x0+level*3, y, subj, clrSubj, clrOut);
3203 // nesting dots
3204 if (level) {
3205 immutable uint clrDot = getColor(buildStyle("dots"));
3206 foreach (immutable dx; 0..level) gxPutPixel(gxClipRect.x0+1+dx*3, y+gxTextHeightUtf/2, clrDot);
3209 // deleted strike line
3210 if (isSoftDeleted(appearance)) {
3211 immutable uint clrLine = getColor(buildStyle("strike-line"));
3212 if (!gxIsTransparent(clrLine)) {
3213 gxClipRect.x0 = origX0;
3214 gxClipRect.x1 = origX1;
3215 gxHLine(gxClipRect.x0, y+gxTextHeightUtf/2, gxClipRect.x1-gxClipRect.x0+1, clrLine);
3216 if (appearance == Appearance.SoftDeletePurge) {
3217 gxHLine(gxClipRect.x0, y+gxTextHeightUtf/2+1, gxClipRect.x1-gxClipRect.x0+1, clrLine);
3222 y += gxTextHeightUtf;
3226 // draw progressbar
3227 if (msglistCurrUId) {
3228 gxClipRect.x0 = origX0;
3229 gxClipRect.x1 = origX1+5;
3230 gxClipRect.y0 = origY0;
3231 gxClipRect.y1 = origY1;
3232 gxDrawScrollBar(GxRect(gxClipRect.x1-5, gxClipRect.y0, 4, gxClipRect.height-1),
3233 cast(int)chiroGetTreePaneTableCount()-1, curridx);
3236 drawArticle(msglistCurrUId);
3239 // ////////////////////////////////////////////////////////////////// //
3240 void drawFolders () {
3241 immutable uint clrNormal = getColor("grouplist-normal-text");
3242 immutable uint clrNormalCursor = getColor("grouplist-cursor-normal-text");
3243 immutable uint clrNormalChild = getColor("grouplist-normal-child-text");
3244 immutable uint clrNormalChildCursor = getColor("grouplist-cursor-normal-child-text");
3245 immutable uint clrDots = getColor("grouplist-dots");
3246 immutable uint clrNormOutline = getColor("grouplist-outline");
3247 immutable uint clrCurOutline = getColor("grouplist-cursor-outline");
3249 folderMakeCurVisible();
3250 int ofsx = 2;
3251 int ofsy = 1;
3252 foreach (immutable idx, const FolderInfo fi; folderList) {
3253 if (idx < folderTopIndex) continue;
3254 if (ofsy >= screenHeight) break;
3255 gxClipReset();
3257 immutable int depth = fi.depth;
3258 uint clr = (depth ? clrNormalChild : clrNormal);
3259 uint clrOut = clrNormOutline;
3261 immutable bool isCursor = (idx == folderCurrIndex);
3262 if (isCursor) {
3263 gxFillRect(0, ofsy-1, guiGroupListWidth, (gxTextHeightUtf+2), getColor("grouplist-cursor-back"));
3264 clr = (depth ? clrNormalChildCursor : clrNormalCursor);
3265 clrOut = clrCurOutline;
3267 gxClipRect.x0 = ofsx-1;
3268 gxClipRect.y0 = ofsy;
3269 gxClipRect.x1 = guiGroupListWidth-3;
3271 if (fi.unreadCount) {
3272 clr = getColor(isCursor ? "grouplist-cursor-unread-text" : "grouplist-unread-text");
3273 } else if (depth == 0) {
3274 if (fi.name == "#spam") clr = getColor(isCursor ? "grouplist-cursor-spam-text" : "grouplist-spam-text");
3275 else if (fi.name == "/accounts") clr = getColor(isCursor ? "grouplist-cursor-accounts-text" : "grouplist-accounts-text");
3276 } else if (depth == 1 && fi.name.startsWith("/accounts/")) {
3277 clr = getColor(isCursor ? "grouplist-cursor-accounts-child-text" : "grouplist-accounts-child-text");
3278 } else if (fi.name.startsWith("/accounts/") && fi.name.endsWith("/inbox")) {
3279 clr = getColor(isCursor ? "grouplist-cursor-inbox-text" : "grouplist-inbox-text");
3281 foreach (immutable dd; 0..depth) gxPutPixel(ofsx+dd*6+2, ofsy+gxTextHeightUtf/2, clrDots);
3282 gxDrawTextOutUtf(ofsx+depth*6, ofsy, fi.visname, clr, clrOut);
3283 ofsy += gxTextHeightUtf+2;
3287 drawFolders();
3288 drawThreadList();
3289 //setupTrayAnimation();
3291 version(test_round_rect) {
3292 gxClipReset();
3293 gxFillRoundedRect(lastMouseX-16, lastMouseY-16, 128, 96, rrad, /*gxSolidWhite*/gxRGBA!(0, 255, 0, 127));
3294 //gxDrawRoundedRect(lastMouseX-16, lastMouseY-16, 128, 96, rrad, gxRGBA!(0, 255, 0, 127));
3298 version(test_round_rect) {
3299 int rrad = 16;
3302 override bool onKeySink (KeyEvent event) {
3303 if (event.pressed) {
3304 version(test_round_rect) {
3305 if (event == "Plus") { ++rrad; postScreenRebuild(); return true; }
3306 if (event == "Minus") { --rrad; postScreenRebuild(); return true; }
3308 if (dbg_dump_keynames) conwriteln("key: ", event.toStr, ": ", event.modifierState&ModifierState.windows);
3309 //foreach (const ref kv; mainAppKeyBindings.byKeyValue) if (event == kv.key) concmd(kv.value);
3310 char[64] kname;
3311 if (auto cmdp = event.toStrBuf(kname[]) in mainAppKeyBindings) {
3312 concmd(*cmdp);
3313 return true;
3315 // debug
3317 if (event == "S-Up") {
3318 if (folderTop > 0) --folderTop;
3319 postScreenRebuild();
3320 return true;
3322 if (event == "S-Down") {
3323 if (folderTop+1 < folders.length) ++folderTop;
3324 postScreenRebuild();
3325 return true;
3328 //if (event == "Tab") { new PostWindow(); return true; }
3329 //if (event == "Tab") { new SelectPopBoxWindow(null); return true; }
3331 return super.onKeySink(event);
3334 // returning `false` to avoid screen rebuilding by dispatcher
3335 override bool onMouseSink (MouseEvent event) {
3336 int mx = event.x;
3337 int my = event.y;
3338 // button press
3339 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
3340 // select folder
3341 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < screenHeight) {
3342 uint fnum = my/(gxTextHeightUtf+2)+folderTopIndex;
3343 if (fnum >= 0 && fnum != folderCurrIndex && folderCurrIndex < folderList.length) {
3344 folderCurrIndex = fnum;
3345 postScreenRebuild();
3347 return false;
3349 // select post
3350 if (mx > guiGroupListWidth && mx < screenWidth && my >= 0 && my < guiThreadListHeight) {
3351 if (lastDecodedTid != 0) {
3352 my /= gxTextHeightUtf;
3353 // find indicies
3354 int topidx, curridx;
3355 getAndFixThreadListIndicies(out topidx, out curridx);
3356 int newidx = topidx+my;
3357 if (curridx != newidx) {
3358 uint newuid = chiroGetTreePaneTableIndex2Uid(newidx);
3359 if (newuid && newuid != msglistCurrUId) {
3360 chiroSetMessageRead(lastDecodedTid, newuid);
3361 msglistCurrUId = newuid;
3362 setupTrayAnimation();
3363 postScreenRebuild();
3366 return false;
3368 } else {
3369 auto uidx = findUrlIndexAt(mx, my);
3370 if (uidx != lastUrlIndex || lastUrlIndex >= 0) { lastUrlIndex = uidx; postScreenRebuild(); }
3373 // wheel
3374 if (event.type == MouseEventType.buttonPressed &&
3375 (event.button == MouseButton.wheelUp || event.button == MouseButton.wheelDown))
3377 // folder
3378 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < screenHeight) {
3379 if (event.button == MouseButton.wheelUp) {
3380 if (folderScrollUp()) postScreenRebuild();
3382 if (folderCurrIndex > 0) {
3383 --folderCurrIndex;
3384 postScreenRebuild();
3387 } else {
3388 if (folderScrollDown()) postScreenRebuild();
3390 if (folderCurrIndex+1 < folderList.length) {
3391 ++folderCurrIndex;
3392 postScreenRebuild();
3396 return false;
3398 // post
3399 if (mx > guiGroupListWidth && mx < screenWidth && my >= 0 && my < guiThreadListHeight) {
3400 if (event.button == MouseButton.wheelUp) {
3401 //if (threadListUp()) postScreenRebuild();
3402 if (threadListScrollUp(movecurrent:false)) postScreenRebuild();
3403 } else {
3404 //if (threadListDown()) postScreenRebuild();
3405 if (threadListScrollDown(movecurrent:false)) postScreenRebuild();
3407 return false;
3409 // text
3410 if (mx > guiGroupListWidth && mx < screenWidth && my > guiThreadListHeight && my < screenHeight) {
3411 enum ScrollLines = 2;
3412 if (event.button == MouseButton.wheelUp) scrollBy(-ScrollLines); else scrollBy(ScrollLines);
3413 postScreenRebuild();
3414 return false;
3417 // button release
3418 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
3419 // try url
3420 auto uidx = findUrlIndexAt(mx, my);
3421 auto url = findUrlAt(mx, my);
3422 if (url !is null) {
3423 if (url.isAttach) {
3424 if (event.modifierState&(ModifierState.alt|ModifierState.shift|ModifierState.ctrl)) {
3425 concmdf!"attach_save %s"(url.attachnum);
3426 } else {
3427 concmdf!"attach_view %s"(url.attachnum);
3429 } else {
3430 if (event.modifierState&(ModifierState.shift|ModifierState.ctrl)) {
3431 //conwriteln("link-to-clipboard: <", url.url, ">");
3432 setClipboardText(vbwin, url.url.getData.idup); // it is safe to cast here
3433 setPrimarySelection(vbwin, url.url.getData.idup); // it is safe to cast here
3434 conwriteln("url copied to the clipboard.");
3435 } else {
3436 //conwriteln("link-open: <", url.url, ">");
3437 concmdf!"open_url \"%s\" %s"(url.url, ((event.modifierState&ModifierState.alt) != 0));
3440 postScreenRebuild();
3441 } else {
3442 if (lastUrlIndex >= 0) postScreenRebuild();
3444 lastUrlIndex = uidx;
3446 if (event.type == MouseEventType.motion) {
3447 auto uidx = findUrlIndexAt(mx, my);
3448 if (uidx != lastUrlIndex) { lastUrlIndex = uidx; postScreenRebuild(); return false; }
3452 if (event.type == MouseEventType.buttonPressed || event.type == MouseEventType.buttonReleased) {
3453 postScreenRebuild();
3454 } else {
3455 // for OpenGL, this rebuilds the whole screen anyway
3456 postScreenRepaint();
3460 return false;
3465 // ////////////////////////////////////////////////////////////////////////// //
3466 __gshared LockFile mainLockFile;
3469 void checkMainLockFile () {
3470 import std.path : buildPath;
3471 mainLockFile = LockFile(buildPath(mailRootDir, ".chiroptera2.lock"));
3472 if (!mainLockFile.tryLock) {
3473 mainLockFile.close();
3474 //assert(0, "already running");
3475 conwriteln("another copy of Chiroptera is running, disabling updater.");
3476 receiverDisable();
3481 void main (string[] args) {
3483 import etc.linux.memoryerror;
3484 bool setMH = true;
3485 int idx = 1;
3486 while (idx < args.length) {
3487 string a = args[idx++];
3488 if (a == "--") break;
3489 if (a == "--gdb") {
3490 setMH = false;
3491 --idx;
3492 foreach (immutable c; idx+1..args.length) args[c-1] = args[c];
3495 if (setMH) registerMemoryErrorHandler();
3498 defaultColorStyle.parseStyle(ChiroStyle);
3500 glconAllowOpenGLRender = false;
3502 checkMainLockFile();
3503 scope(exit) mainLockFile.close();
3505 sdpyWindowClass = "Chiroptera";
3506 //glconShowKey = "M-Grave";
3508 initConsole();
3509 //FIXME
3510 //hitwitInitConsole();
3512 clearBindings();
3513 setupDefaultBindings();
3515 concmd("exec chiroptera.rc tan");
3516 concmd("load_style userstyle.rc");
3518 //FIXME
3519 //scanFolders();
3521 //FIXME:concmdf!"exec %s/accounts.rc tan"(mailRootDir);
3522 //FIXME:concmdf!"exec %s/addressbook.rc tan"(mailRootDir);
3523 //FIXME:concmdf!"exec %s/filters.rc tan"(mailRootDir);
3524 //FIXME:concmdf!"exec %s/highlights.rc tan"(mailRootDir);
3525 //FIXME:concmdf!"exec %s/twits.rc tan"(mailRootDir);
3526 //FIXME:concmdf!"exec %s/twit_threads.rc tan"(mailRootDir);
3527 //FIXME:concmdf!"exec %s/auto_twits.rc tan"(mailRootDir);
3528 //FIXME:concmdf!"exec %s/auto_twit_threads.rc tan"(mailRootDir);
3529 //FIXME:concmdf!"exec %s/repreps.rc tan"(mailRootDir);
3530 conProcessQueue(); // load config
3531 conProcessArgs!true(args);
3533 chiroOpenStorageDB();
3534 chiroOpenViewDB();
3535 chiroOpenConfDB();
3536 //ChiroTimerEnabled = true;
3537 //ChiroTimerExEnabled = true;
3539 rescanFolders();
3541 egraCreateSystemWindow("Chiroptera", allowResize:true);
3543 static if (is(typeof(&vbwin.closeQuery))) {
3544 vbwin.closeQuery = delegate () { concmd("quit"); egraPostDoConCommands(); };
3548 vbwin.windowResized = delegate (int wdt, int hgt) {
3549 egraSdpyOnWindowResized(wdt, hgt);
3551 // TODO: fix gui sizes
3552 if (vbwin.closed) return;
3554 double glwFrac = cast(double)guiGroupListWidth/screenWidth;
3555 double tlhFrac = cast(double)guiThreadListHeight/screenHeight;
3557 if (wdt < screenEffScale*32) wdt = screenEffScale;
3558 if (hgt < screenEffScale*32) hgt = screenEffScale;
3559 int newwdt = (wdt+screenEffScale-1)/screenEffScale;
3560 int newhgt = (hgt+screenEffScale-1)/screenEffScale;
3562 guiGroupListWidth = cast(int)(glwFrac*newwdt+0.5);
3563 guiThreadListHeight = cast(int)(tlhFrac*newhgt+0.5);
3565 if (guiGroupListWidth < 12) guiGroupListWidth = 12;
3566 if (guiThreadListHeight < 16) guiThreadListHeight = 16;
3570 vbwin.addEventListener((QuitEvent evt) {
3571 if (vbwin.closed) return;
3572 if (isQuitRequested) { vbwin.close(); return; }
3573 vbwin.close();
3577 vbwin.addEventListener((TrayAnimationStepEvent evt) {
3578 if (vbwin.closed) return;
3579 if (isQuitRequested) { vbwin.close(); return; }
3580 trayDoAnimationStep();
3584 HintWindow uphintWindow;
3586 vbwin.addEventListener((UpdatingAccountEvent evt) {
3587 DynStr accName = chiroGetAccountName(evt.accid);
3588 if (accName.length) {
3589 DynStr msg = "updating: ";
3590 msg ~= accName;
3591 if (uphintWindow !is null) {
3592 uphintWindow.message = msg;
3593 } else {
3594 uphintWindow = new HintWindow(msg);
3595 uphintWindow.y0 = guiThreadListHeight+1+(3*gxTextHeightUtf+2-uphintWindow.height)/2;
3597 postScreenRebuild();
3601 vbwin.addEventListener((UpdatingAccountCompleteEvent evt) {
3602 if (uphintWindow is null) return;
3603 DynStr accName = chiroGetAccountName(evt.accid);
3604 if (accName.length) {
3605 DynStr msg = "done: ";
3606 msg ~= accName;
3607 uphintWindow.message = msg;
3608 postScreenRebuild();
3612 vbwin.addEventListener((UpdatingCompleteEvent evt) {
3613 if (uphintWindow) {
3614 uphintWindow.close();
3615 uphintWindow = null;
3617 if (vbwin is null || vbwin.closed) return;
3618 setupTrayAnimation(); // check if we have to start/stop animation, and do it
3619 postScreenRebuild();
3622 vbwin.addEventListener((TagThreadsUpdatedEvent evt) {
3623 if (vbwin is null || vbwin.closed) return;
3624 if (mainPane is null) return;
3625 if (evt.tagid && mainPane.lastDecodedTid == evt.tagid) {
3626 // force view pane rebuild
3627 mainPane.switchToFolderTid(evt.tagid, forced:true);
3628 postScreenRebuild();
3633 ProgressWindow recalcHintWindow;
3635 vbwin.addEventListener((RecalcAllTwitsEvent evt) {
3636 if (vbwin !is null && !vbwin.closed) {
3637 glconHide();
3638 if (recalcHintWindow !is null) recalcHintWindow.close();
3639 recalcHintWindow = new ProgressWindow("recalculating twits");
3640 egraRebuildScreen();
3641 } else {
3642 recalcHintWindow = null;
3645 disableMailboxUpdates();
3646 scope(exit) enableMailboxUpdates();
3647 chiroRecalcAllTwits((msg, curr, total) {
3648 if (recalcHintWindow is null) return;
3649 if (recalcHintWindow.setProgress(msg, curr, total)) {
3650 egraRebuildScreen();
3654 if (vbwin !is null && !vbwin.closed) {
3655 if (recalcHintWindow !is null) recalcHintWindow.close();
3656 if (mainPane !is null) mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
3657 postScreenRebuild();
3661 vbwin.addEventListener((ArticleTextScrollEvent evt) {
3662 if (vbwin is null || vbwin.closed) return;
3663 if (mainPane is null) return;
3664 mainPane.doScrollStep();
3667 vbwin.addEventListener((MarkAsUnreadEvent evt) {
3668 if (vbwin is null || vbwin.closed) return;
3669 if (mainPane is null) return;
3670 //conwriteln("unread timer fired");
3671 if (mainPane.lastDecodedTid == evt.tagid && evt.uid == mainPane.msglistCurrUId) {
3672 //conwriteln("*** unread timer hit!");
3673 chiroSetMessageRead(evt.tagid, evt.uid);
3674 setupTrayAnimation();
3675 postScreenRebuild();
3679 void firstTimeInit () {
3680 // create notification icon
3681 if (trayicon is null) {
3682 auto drv = vfsAddPak(wrapMemoryRO(iconsZipData[]), "", "databinz/icons.zip:");
3683 scope(exit) vfsRemovePak(drv);
3684 try {
3685 foreach (immutable idx; 0..6) {
3686 string fname = "databinz/icons.zip:icons";
3687 if (idx == 0) {
3688 fname ~= "/main.png";
3689 } else {
3690 import std.format : format;
3691 fname = "%s/bat%s.png".format(fname, idx-1);
3693 auto fl = VFile(fname);
3694 if (fl.size == 0 || fl.size > 1024*1024) throw new Exception("fucked icon");
3695 auto pngraw = new ubyte[](cast(uint)fl.size);
3696 fl.rawReadExact(pngraw);
3697 auto img = readPng(pngraw);
3698 if (img is null) throw new Exception("fucked icon");
3699 icons[idx] = imageFromPng(img);
3701 foreach (immutable idx, MemoryImage img; icons[]) {
3702 trayimages[idx] = Image.fromMemoryImage(img);
3704 vbwin.icon = icons[0];
3705 trayicon = new NotificationAreaIcon("Chiroptera", trayimages[0], (MouseButton button) {
3706 scope(exit) if (!conQueueEmpty()) egraPostDoConCommands();
3707 if (button == MouseButton.left) vbwin.switchToWindow();
3708 if (button == MouseButton.middle) concmd("quit");
3710 setupTrayAnimation();
3711 flushGui(); // or it may not redraw itself
3712 } catch (Exception e) {
3713 conwriteln("ERROR loading icons: ", e.msg);
3718 vbwin.visibleForTheFirstTime = delegate () {
3719 egraFirstTimeInit();
3720 firstTimeInit();
3723 mainPane = new MainPaneWindow();
3724 egraSkipScreenClear = true; // main pane is fullscreen
3726 postScreenRebuild();
3727 repostHideMouse();
3729 receiverInit();
3731 MonoTime lastCollect = MonoTime.currTime;
3732 vbwin.eventLoop(1000*10,
3733 delegate () {
3734 egraProcessConsole();
3735 if (mainPane !is null) {
3736 if (mainPane.isViewChanged) postScreenRebuild();
3737 mainPane.checkSaveState();
3738 setupTrayAnimation();
3741 immutable ctt = MonoTime.currTime;
3742 if ((ctt-lastCollect).total!"minutes" >= 1) {
3743 import core.memory : GC;
3744 lastCollect = ctt;
3745 GC.collect();
3746 GC.minimize();
3750 delegate (KeyEvent event) {
3751 egraOnKey(event);
3752 if (mainPane !is null && mainPane.isViewChanged) postScreenRebuild();
3754 delegate (MouseEvent event) {
3755 egraOnMouse(event);
3756 if (mainPane !is null && mainPane.isViewChanged) postScreenRebuild();
3758 delegate (dchar ch) {
3759 egraOnChar(ch);
3760 if (mainPane !is null && mainPane.isViewChanged) postScreenRebuild();
3764 mainPane.saveCurrentState();
3766 trayimages[] = null;
3767 if (trayicon !is null && !trayicon.closed) { trayicon.close(); trayicon = null; }
3768 flushGui();
3769 receiverDeinit();
3770 conProcessQueue(int.max/4);