use new "accept/cancel" egra mechanics in GUI
[chiroptera.git] / chiroptera.d
blobebaa41a29ca56d1cb28ab18f2d1377c1b2a08c2f
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-back: #222;
68 // group with unread messages
69 grouplist-unread-text: #0ff;
70 // normal group
71 grouplist-normal-text: rgb(255, 187, 0);
72 grouplist-normal-child-text: rgb(225, 97, 0);
73 // spam group
74 grouplist-spam-text: #800;
75 // main accounts group
76 grouplist-accounts-text: rgb(220, 220, 0);
77 grouplist-accounts-child-text: rgb(160, 160, 250);
78 // account inbox group
79 grouplist-inbox-text: rgb(90, 90, 180);
81 grouplist-dots: rgb(80, 80, 80);
83 .cursor {
84 grouplist-back: #088;
85 grouplist-outline: black;
89 //// thread list ////
90 threadlist-divline: white;
91 threadlist-back: #222;
92 threadlist-dots: #444;
94 .normal {
95 threadlist-back: transparent;
96 threadlist-subj-text: rgb(215, 87, 0);
97 threadlist-from-text: rgb(215, 87, 0);
98 threadlist-mail-text: rgb(155, 27, 0);
99 threadlist-time-text: rgb(215, 87, 0);
100 threadlist-strike-line: transparent;
103 .unread {
104 threadlist-back: transparent;
105 threadlist-subj-text: white;
106 threadlist-from-text: white;
107 threadlist-mail-text: yellow;
108 threadlist-time-text: white;
109 threadlist-strike-line: transparent;
112 .soft-del {
113 threadlist-back: transparent;
114 threadlist-subj-text: #800;
115 threadlist-from-text: #800;
116 threadlist-mail-text: #800;
117 threadlist-time-text: #800;
118 threadlist-strike-line: #800;
121 .hard-del {
122 threadlist-back: transparent;
123 threadlist-subj-text: red;
124 threadlist-from-text: red;
125 threadlist-mail-text: red;
126 threadlist-time-text: red;
127 threadlist-strike-line: red;
130 .twit {
131 threadlist-back: transparent;
132 threadlist-subj-text: #400;
133 threadlist-from-text: #400;
134 threadlist-mail-text: #400;
135 threadlist-time-text: #400;
136 threadlist-strike-line: transparent;
139 .cursor-normal {
140 threadlist-back: #088;
141 threadlist-subj-text: @.normal;
142 threadlist-from-text: @.normal;
143 threadlist-mail-text: @.normal;
144 threadlist-time-text: @.normal;
145 threadlist-strike-line: @normal;
146 threadlist-outline: black;
149 .cursor-unread {
150 threadlist-back: @.cursor-normal;
151 threadlist-subj-text: @.unread;
152 threadlist-from-text: @.unread;
153 threadlist-mail-text: @.unread;
154 threadlist-time-text: @.unread;
155 threadlist-strike-line: @.unread;
156 threadlist-outline: black;
159 .cursor-soft-del {
160 threadlist-back: #066;
161 threadlist-subj-text: @.soft-del;
162 threadlist-from-text: @.soft-del;
163 threadlist-mail-text: @.soft-del;
164 threadlist-time-text: @.soft-del;
165 threadlist-strike-line: @.soft-del;
168 .cursor-hard-del {
169 threadlist-back: @.cursor-soft-del;
170 threadlist-subj-text: @.hard-del;
171 threadlist-from-text: @.hard-del;
172 threadlist-mail-text: @.hard-del;
173 threadlist-time-text: @.hard-del;
174 threadlist-strike-line: @.hard-del;
177 .cursor-twit {
178 threadlist-back: #066;
179 threadlist-subj-text: @.twit;
180 threadlist-from-text: @.twit;
181 threadlist-mail-text: @.twit;
182 threadlist-time-text: @.twit;
183 threadlist-strike-line: @.twit;
187 //// message header ////
188 msg-header-back: rgb(20, 20, 20);
189 msg-header-from: #088;
190 msg-header-to: #088;
191 msg-header-subj: #088;
192 msg-header-date: #088;
193 msg-header-divline: #bbb;
196 //// message text ////
197 msg-text-back: rgb(37, 37, 37);
198 msg-text-text: rgb(174, 174, 174);
199 msg-text-quote0: #880;
200 msg-text-quote1: #088;
201 msg-text-link: rgb(0, 200, 200);
202 msg-text-html-sign: rgb(128, 0, 128);
204 .hover {
205 msg-text-link: rgb(0, 255, 255);
208 .pressed {
209 msg-text-link: rgb(255, 0, 255);
212 //// message text twit ////
213 //twit-shade: rgba(0, 0, 80, 127);
214 twit-shade: rgb(0, 0, 40);
215 twit-text: red;
216 twit-outline: black;
220 HintWindow {
221 frame: white;
222 title-back: @frame;
223 title-text: black;
225 back: rgb(0, 0, 80);
226 text: rgb(155, 155, 155);
230 MessageWindow {
231 frame: white;
232 title-back: @frame;
233 title-text: black;
235 back: rgb(0, 0, 80);
236 text: rgb(255, 255, 0);
237 bar-back: rgb(90, 90, 180);
238 back: red;
243 // ////////////////////////////////////////////////////////////////////////// //
244 private __gshared bool ChiroTimerExEnabled = false;
247 // ////////////////////////////////////////////////////////////////////////// //
248 public __gshared string mailRootDir = "/mnt/bigass/Mail";
251 shared static this () {
252 import core.stdc.stdlib : getenv;
253 const(char)* home = getenv("HOME");
254 if (home !is null && home[0] == '/' && home[1] && home[1] != '/') {
255 import std.string : fromStringz;
256 mailRootDir = home.fromStringz.idup;
257 if (mailRootDir.length == 0) assert(0, "wtf?!");
258 if (mailRootDir[$-1] != '/') mailRootDir ~= "/";
259 mailRootDir ~= "Mail";
264 // ////////////////////////////////////////////////////////////////////////// //
265 __gshared NotificationAreaIcon trayicon;
266 __gshared Image[6] trayimages;
267 __gshared MemoryImage[6] icons; // 0: normal
270 // ////////////////////////////////////////////////////////////////////////// //
271 // with tagid
272 struct ArticleId {
273 uint tagid = 0;
274 uint uid = 0;
276 bool valid () const nothrow @safe @nogc { pragma(inline, true); return (tagid && uid); }
277 void clear () nothrow @safe @nogc { pragma(inline, true); tagid = uid = 0; }
279 bool opEqual (const ref ArticleId other) const nothrow @safe @nogc {
280 pragma(inline, true);
281 return (valid && other.valid && tagid == other.tagid && uid == other.uid);
286 // ////////////////////////////////////////////////////////////////////////// //
287 static immutable ubyte[] iconsZipData = cast(immutable(ubyte)[])import("databin/icons.zip");
290 // ////////////////////////////////////////////////////////////////////////// //
291 struct MailReplacement {
292 string oldmail;
293 string newmail;
294 string newname;
297 __gshared MailReplacement[string] repmailreps;
300 // ////////////////////////////////////////////////////////////////////////// //
301 string getBrowserCommand (bool forceOpera) {
302 __gshared string browser;
303 if (forceOpera) return "opera";
304 if (browser.length == 0) {
305 import core.stdc.stdlib : getenv;
306 const(char)* evar = getenv("BROWSER");
307 if (evar !is null && evar[0]) {
308 import std.string : fromStringz;
309 browser = evar.fromStringz.idup;
310 } else {
311 browser = "opera";
314 return browser;
318 // ////////////////////////////////////////////////////////////////////////// //
320 private void setXStr (ref char[] dest, SQ3Text src) {
321 delete dest;
322 if (src.length == 0) return;
323 dest = new char[src.length];
324 dest[] = src[];
329 // ////////////////////////////////////////////////////////////////////////// //
330 class FolderInfo {
331 uint tagid; // 0 means "ephemeral"
332 DynStr name;
333 const(char)[] visname; // slice of the `name`
334 int depth;
335 uint unreadCount;
336 // used in rescanner;
337 bool seen;
339 ~this () nothrow @trusted @nogc { clear(); }
341 void clear () nothrow @trusted @nogc {
342 visname = null;
343 name.clear();
346 // ephemeral folders doesn't exist, they are here only for visual purposes
347 bool ephemeral () const nothrow @safe @nogc { pragma(inline, true); return (tagid == 0); }
349 void calcDepthVisName () {
350 depth = 0;
351 visname = name[0..$];
352 if (visname.length == 0 || visname == "/" || visname[0] == '#') return;
353 visname = visname[1..$];
354 foreach (immutable char ch; visname) if (ch == '/') ++depth;
355 auto spos = visname.lastIndexOf('/');
356 if (spos >= 0) visname = visname[spos+1..$];
359 static int findByFullName (const(char)[] aname) {
360 if (aname.length == 0) return -1;
361 foreach (immutable idx, const FolderInfo fi; folderList) {
362 if (fi.name == aname) return cast(int)idx;
364 return -1;
367 bool needEphemeral () const {
368 if (depth == 0) return false;
369 assert(name.length > 1 && name[0] == '/');
370 const(char)[] n = name[0..$];
371 while (n.length) {
372 if (findByFullName(n) < 0) return true;
373 auto spos = n.lastIndexOf('/');
374 if (spos <= 0) return false;
375 n = n[0..spos];
377 return false;
380 void createEphemerals () const {
381 if (depth == 0) return;
382 assert(name.length > 1 && name[0] == '/');
383 const(char)[] n = name[0..$];
384 while (n.length) {
385 auto spos = n.lastIndexOf('/');
386 if (spos <= 0) break;
387 n = n[0..spos];
388 //conwriteln(" n=<", n, ">; spos=", spos);
389 if (findByFullName(n) < 0) {
390 //conwriteln(" creating: '", n, "'");
391 //foreach (const FolderInfo nfi; folderList) conwriteln(" <", nfi.name, "> : <", nfi.visname, "> : ", nfi.depth);
392 FolderInfo newfi = new FolderInfo;
393 newfi.tagid = 0;
394 newfi.name = n;
395 newfi.unreadCount = 0;
396 newfi.seen = true;
397 newfi.calcDepthVisName();
398 folderList ~= newfi;
404 __gshared FolderInfo[] folderList;
405 __gshared uint folderDataVersion = uint.max;
408 //FIXME: make this faster
409 //FIXME: force-append account folders
410 // returns `true` if something was changed
411 bool rescanFolders () {
412 import std.conv : to;
413 bool res = false;
415 if (folderList.length != 0 && dbView.getDataVersion() == folderDataVersion) return false; // nothing to do here
417 bool needsort = false;
418 foreach (FolderInfo fi; folderList) fi.seen = false;
420 // unhide any folder tags with unread messages
421 static stmtUnhide = LazyStatement!"View"(`
422 UPDATE tagnames
424 hidden=0
425 WHERE
426 hidden=1 AND
427 (tag='#spam' OR (tag<>'' AND SUBSTR(tag, 1, 1)='/')) AND
428 EXISTS (SELECT uid FROM threads WHERE tagid=tagnames.tagid AND appearance=`~(cast(int)Appearance.Unread).to!string~`)
429 ;`);
430 stmtUnhide.st.doAll();
432 static auto stmtGet = LazyStatement!"View"(`
433 SELECT
434 tagid AS tagid
435 , tag AS name
436 FROM tagnames
437 WHERE
438 tag='#spam' OR
439 (hidden=0 AND tag<>'' AND SUBSTR(tag, 1, 1)='/')
440 ;`);
442 static auto stmtGetUnread = LazyStatement!"View"(`
443 SELECT
444 COUNT(uid) AS unread
445 FROM threads
446 WHERE tagid=:tagid AND appearance=`~(cast(int)Appearance.Unread).to!string~`
447 ;`);
449 foreach (auto row; stmtGet.st.range) {
450 bool append = true;
451 uint tagid = row.tagid!uint;
452 foreach (FolderInfo fi; folderList) {
453 if (fi.tagid == tagid) {
454 append = false;
455 fi.seen = true;
456 if (fi.name != row.name!SQ3Text) {
457 fi.name = row.name!SQ3Text;
458 fi.calcDepthVisName();
459 needsort = true;
461 break;
464 if (append) {
465 needsort = true;
466 FolderInfo newfi = new FolderInfo();
467 newfi.tagid = tagid;
468 newfi.name = row.name!SQ3Text;
469 newfi.unreadCount = 0;
470 newfi.seen = true;
471 newfi.calcDepthVisName();
472 folderList ~= newfi;
476 // remove unseen folders
477 for (usize f = 0; f < folderList.length; ) {
478 if (!folderList[f].seen && !folderList[f].ephemeral) {
479 needsort = true;
480 folderList[f].clear();
481 delete folderList[f];
482 foreach (immutable c; f+1..folderList.length) folderList[c-1] = folderList[c];
483 folderList[$-1] = null;
484 folderList.length -= 1;
485 } else {
486 ++f;
490 if (needsort) {
491 // remove all epemerals
492 for (usize f = 0; f < folderList.length; ) {
493 if (folderList[f].ephemeral) {
494 folderList[f].clear();
495 delete folderList[f];
496 foreach (immutable c; f+1..folderList.length) folderList[c-1] = folderList[c];
497 folderList[$-1] = null;
498 folderList.length -= 1;
499 } else {
500 ++f;
504 // readd all epemerals
505 for (;;) {
506 bool again = false;
507 foreach (FolderInfo fi; folderList) {
508 if (fi.needEphemeral) {
509 //conwriteln("ephemeral for '", fi.name, "'");
510 again = true;
511 fi.createEphemerals();
512 break;
515 if (!again) break;
518 static bool isAccount (const(char)[] s) pure nothrow @trusted @nogc {
519 if (!s.startsWith("/accounts")) return false;
520 return (s.length == 9 || s[9] == '/');
523 import std.algorithm.sorting : sort;
524 folderList.sort!((const FolderInfo a, const FolderInfo b) {
525 if (a.name == b.name) return false;
526 if (isAccount(a.name) && !isAccount(b.name)) return true; // a < b
527 if (!isAccount(a.name) && isAccount(b.name)) return false; // a >= b
528 if (a.name[0] == '#' && b.name[0] != '#') return false; // a >= b
529 if (a.name[0] != '#' && b.name[0] == '#') return true; // a < b
530 return (a.name < b.name);
532 res = true;
535 // check unread counts
536 foreach (FolderInfo fi; folderList) {
537 if (fi.ephemeral) continue;
538 foreach (auto row; stmtGetUnread.st.bind(":tagid", fi.tagid).range) {
539 if (fi.unreadCount != row.unread!uint) {
540 res = true;
541 fi.unreadCount = row.unread!uint;
546 setupTrayAnimation();
547 folderDataVersion = dbView.getDataVersion();
548 //conwriteln("ver=", folderDataVersion);
549 return res;
553 // ////////////////////////////////////////////////////////////////////////// //
554 __gshared bool dbg_dump_keynames;
557 // ////////////////////////////////////////////////////////////////////////// //
558 class TrayAnimationStepEvent {}
559 __gshared TrayAnimationStepEvent evTrayAnimationStep;
560 shared static this () { evTrayAnimationStep = new TrayAnimationStepEvent(); }
562 __gshared int trayAnimationIndex = 0; // 0: no animation
563 __gshared int trayAnimationDir = 1; // direction
564 __gshared uint trayAnimDataVersion = uint.max;
567 // ////////////////////////////////////////////////////////////////////////// //
568 void trayPostAnimationEvent () {
569 if (vbwin !is null && !vbwin.eventQueued!TrayAnimationStepEvent) vbwin.postTimeout(evTrayAnimationStep, 100);
573 void trayDoAnimationStep () {
574 if (trayicon is null || trayicon.closed) return; // no tray icon
575 if (vbwin is null || vbwin.closed) return;
576 if (trayAnimationIndex == 0) return; // no animation
577 trayPostAnimationEvent();
578 if (trayAnimationDir < 0) {
579 if (--trayAnimationIndex == 1) trayAnimationDir = 1;
580 } else {
581 if (++trayAnimationIndex == trayimages.length-1) trayAnimationDir = -1;
583 trayicon.icon = trayimages[trayAnimationIndex];
584 //vbwin.icon = icons[trayAnimationIndex];
585 //vbwin.sendDummyEvent(); // or it won't redraw itself
586 //flushGui(); // or it may not redraw itself
590 // ////////////////////////////////////////////////////////////////////////// //
591 void trayStartAnimation () {
592 if (trayicon is null) return; // no tray icon
593 if (trayAnimationIndex == 0) {
594 trayAnimationIndex = 1;
595 trayAnimationDir = 1;
596 trayicon.icon = trayimages[1];
597 vbwin.icon = icons[1];
598 flushGui(); // or it may not redraw itself
599 trayPostAnimationEvent();
604 void trayStopAnimation () {
605 if (trayicon is null) return; // no tray icon
606 if (trayAnimationIndex != 0) {
607 trayAnimationIndex = 0;
608 trayAnimationDir = 1;
609 trayicon.icon = trayimages[0];
610 vbwin.icon = icons[0];
611 flushGui(); // or it may not redraw itself
616 // check if we have to start/stop animation, and do it
617 void setupTrayAnimation () {
618 import std.conv : to;
619 static auto stmtGetUnread = LazyStatement!"View"(`
620 SELECT 1
621 FROM threads
622 WHERE appearance=`~(cast(int)Appearance.Unread).to!string~`
623 LIMIT 1
624 ;`);
626 if (trayicon is null) return; // no tray icon
628 immutable uint dver = dbView.getDataVersion();
629 //conwriteln("TRAY: dver=", dver, "; trv=", trayAnimDataVersion);
630 if (dver == trayAnimDataVersion) return;
631 trayAnimDataVersion = dver;
633 foreach (auto row; stmtGetUnread.st.range) {
634 //conwriteln("TRAY: start anim!");
635 trayStartAnimation();
636 return;
638 //conwriteln("TRAY: stop anim!");
639 trayStopAnimation();
643 // ////////////////////////////////////////////////////////////////////////// //
644 __gshared string[string] mainAppKeyBindings;
646 void clearBindings () {
647 mainAppKeyBindings.clear();
651 void mainAppBind (ConString kname, ConString concmd) {
652 KeyEvent evt = KeyEvent.parse(kname);
653 if (concmd.length) {
654 mainAppKeyBindings[evt.toStr] = concmd.idup;
655 } else {
656 mainAppKeyBindings.remove(evt.toStr);
661 void mainAppUnbind (ConString kname) {
662 KeyEvent evt = KeyEvent.parse(kname);
663 mainAppKeyBindings.remove(evt.toStr);
667 void setupDefaultBindings () {
668 //mainAppBind("C-L", "dbg_font_window");
669 mainAppBind("C-Q", "quit_prompt");
671 mainAppBind("N", "next_unread ona");
672 mainAppBind("S-N", "next_unread tan");
673 mainAppBind("M", "next_unread ona");
674 mainAppBind("S-M", "next_unread tan");
676 mainAppBind("U", "mark_unread");
677 mainAppBind("R", "mark_read");
679 mainAppBind("Space", "artext_page_down");
680 mainAppBind("S-Space", "artext_page_up");
681 mainAppBind("M-Up", "artext_line_up");
682 mainAppBind("M-Down", "artext_line_down");
684 mainAppBind("Up", "article_prev");
685 mainAppBind("Down", "article_next");
686 mainAppBind("PageUp", "article_pgup");
687 mainAppBind("PageDown", "article_pgdown");
688 mainAppBind("Home", "article_to_first");
689 mainAppBind("End", "article_to_last");
690 mainAppBind("C-Up", "article_scroll_up");
691 mainAppBind("C-Down", "article_scroll_down");
693 mainAppBind("C-PageUp", "folder_prev");
694 mainAppBind("C-PageDown", "folder_next");
696 mainAppBind("M-O", "folder_options");
698 //mainAppBind("C-M-U", "folder_update");
699 mainAppBind("C-S-I", "update_all");
700 mainAppBind("C-H", "article_dump_headers");
702 mainAppBind("C-Backslash", "find_mine");
704 //mainAppBind("C-Slash", "article_to_parent");
705 //mainAppBind("C-Comma", "article_to_prev_sib");
706 //mainAppBind("C-Period", "article_to_next_sib");
708 //mainAppBind("C-Insert", "article_copy_url_to_clipboard");
709 mainAppBind("C-M-K", "article_twit_thread");
710 mainAppBind("T", "article_edit_poster_title");
712 mainAppBind("C-R", "article_reply");
713 mainAppBind("S-R", "article_reply_to_from");
715 mainAppBind("S-P", "new_post");
717 //mainAppBind("S-Enter", "article_open_in_browser");
718 //mainAppBind("M-Enter", "article_open_in_browser tan");
720 mainAppBind("Delete", "article_softdelete_toggle");
721 mainAppBind("C-Delete", "article_harddelete_toggle");
725 // ////////////////////////////////////////////////////////////////////////// //
726 struct ImgViewCommand {
727 string filename;
730 private void imageViewThread (Tid ownerTid) {
731 string fname;
732 try {
733 conwriteln("waiting for the message...");
734 receive(
735 (ImgViewCommand cmd) {
736 fname = cmd.filename;
739 conwriteln("got filename: \"", fname, "\"");
741 try {
742 import std.process;
743 //FIXME: make regvar for image viewer
744 //auto pid = execute(["keh", attfname], null, Config.detached);
745 //pid.wait();
746 import std.stdio : File;
747 auto frd = File("/dev/null");
748 auto fwr = File("/dev/null", "w");
749 auto pid = spawnProcess(["keh", fname], frd, fwr, fwr, null, Config.none/*detached*/);
750 pid.wait();
751 } catch (Exception e) {
752 conwriteln("ERROR executing image viewer: ", e.msg);
754 } catch (Throwable e) {
755 // here, we are dead and fucked (the exact order doesn't matter)
756 //import core.stdc.stdlib : abort;
757 import core.stdc.stdio : fprintf, stderr;
758 //import core.memory : GC;
759 import core.thread : thread_suspendAll;
760 //GC.disable(); // yeah
761 //thread_suspendAll(); // stop right here, you criminal scum!
762 auto s = e.toString();
763 fprintf(stderr, "\n=== FATAL ===\n%.*s\n", cast(uint)s.length, s.ptr);
765 try {
766 import std.file : remove;
767 if (fname.length) {
768 conwriteln("deleting file \"", fname, "\"");
769 remove(fname);
771 } catch (Exception e) {}
775 // ////////////////////////////////////////////////////////////////////////// //
776 void initConsole () {
777 import std.functional : toDelegate;
779 conRegVar!scrollKeepLines("scroll_keep_lines", "number of lines to keep on page up or page down.");
780 conRegVar!unreadTimeoutInterval("t_unread_timeout", "timout to mark keyboard-selected message as unread (<1: don't mark)");
781 conRegVar!preferHtmlContent("prefer_html_content", "prefer html content when both html and plain are present?");
782 conRegVar!detectHtmlContent("detect_html_content", "detect html content in plain text?");
784 conRegFunc!clearBindings("binds_app_clear", "clear main application keybindings");
785 conRegFunc!setupDefaultBindings("binds_app_default", "*append* default application bindings");
786 conRegFunc!mainAppBind("bind_app", "add main application binding");
787 conRegFunc!mainAppUnbind("unbind_app", "remove main application binding");
790 // //////////////////////////////////////////////////////////////////// //
791 conRegFunc!((ConString filename) {
792 if (filename.length == 0) return;
793 import std.path : buildPath;
794 auto fname = buildPath(mailRootDir, filename);
795 char[] text;
796 scope(exit) delete text;
797 bool sayError = false;
798 try {
799 auto fl = VFile(fname);
800 sayError = true;
801 conwriteln("loading style file '", fname, "'...");
802 auto sz = fl.size;
803 if (sz > 1024*1024*32) { conwriteln("ERROR: style file too big!"); return; }
804 if (sz == 0) return;
805 text = new char[cast(usize)sz];
806 fl.rawReadExact(text);
807 fl.close();
808 } catch (Exception) {
809 if (sayError) conwriteln("ERROR reading style file!");
810 return;
812 try {
813 defaultColorStyle.parseStyle(text);
814 } catch (Exception e) {
815 conwriteln("ERROR parsing style: ", e.msg);
817 })("load_style", "load widget style");
820 // //////////////////////////////////////////////////////////////////// //
821 conRegFunc!(() {
822 import core.memory : GC;
823 conwriteln("starting GC collection...");
824 GC.collect();
825 GC.minimize();
826 conwriteln("GC collection complete.");
827 })("gc_collect", "force GC collection cycle");
830 // //////////////////////////////////////////////////////////////////// //
831 conRegFunc!(() {
832 auto qww = new YesNoWindow("Quit?", "Do you really want to quit?", true);
833 qww.onYes = () { concmd("quit"); };
834 //qww.addModal();
835 })("quit_prompt", "quit with prompt");
838 // //////////////////////////////////////////////////////////////////// //
839 conRegFunc!((ConString url, bool forceOpera=false) {
840 if (url.length) {
841 import std.stdio : File;
842 import std.process;
843 try {
844 auto frd = File("/dev/null");
845 auto fwr = File("/dev/null", "w");
846 spawnProcess([getBrowserCommand(forceOpera), url.idup], frd, fwr, fwr, null, Config.detached);
847 } catch (Exception e) {
848 conwriteln("ERROR executing URL viewer (", e.msg, ")");
851 })("open_url", "open given url in a browser");
854 // //////////////////////////////////////////////////////////////////// //
855 conRegFunc!(() {
856 receiverForceUpdateAll();
857 })("update_all", "mark all groups for updating");
860 // //////////////////////////////////////////////////////////////////// //
861 conRegFunc!(() {
862 if (mainPane !is null && mainPane.folderUpOne()) {
863 postScreenRebuild();
865 })("folder_prev", "go to previous group");
867 conRegFunc!(() {
868 if (mainPane !is null && mainPane.folderDownOne()) {
869 postScreenRebuild();
871 })("folder_next", "go to next group");
874 // //////////////////////////////////////////////////////////////////// //
875 conRegFunc!(() {
876 if (vbwin is null || vbwin.closed) return;
877 if (mainPane is null) return;
878 if (chiroGetMessageExactRead(mainPane.lastDecodedTid, mainPane.msglistCurrUId)) {
879 chiroSetMessageUnread(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
880 postScreenRebuild();
881 setupTrayAnimation();
883 })("mark_unread", "mark current message as unread");
885 conRegFunc!(() {
886 if (vbwin is null || vbwin.closed) return;
887 if (mainPane is null) return;
888 if (chiroGetMessageUnread(mainPane.lastDecodedTid, mainPane.msglistCurrUId)) {
889 chiroSetMessageRead(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
890 setupTrayAnimation();
891 postScreenRebuild();
893 })("mark_read", "mark current message as read");
895 conRegFunc!((bool allowNextGroup=false) {
896 if (vbwin is null || vbwin.closed) return;
897 if (mainPane is null) return;
898 // try current group
899 if (mainPane.lastDecodedTid != 0) {
900 auto uid = chiroGetPaneNextUnread(mainPane.msglistCurrUId);
901 if (uid) {
902 // i found her!
903 if (uid == mainPane.msglistCurrUId) return;
904 mainPane.msglistCurrUId = uid;
905 mainPane.threadListPositionDirty();
906 chiroSetMessageRead(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
907 setupTrayAnimation();
908 postScreenRebuild();
909 return;
912 // try other groups?
913 if (!allowNextGroup) return;
914 int idx = mainPane.folderCurrIndex;
915 foreach (; 0..cast(int)folderList.length) {
916 idx = (idx+1)%cast(int)folderList.length;
917 if (idx == mainPane.folderCurrIndex) continue;
918 if (folderList[idx].ephemeral) continue;
919 if (folderList[idx].unreadCount == 0) continue;
920 mainPane.folderSetToIndex(idx);
921 auto uid = chiroGetPaneNextUnread(/*mainPane.msglistCurrUId*/0);
922 if (uid) {
923 // i found her!
924 if (uid == mainPane.msglistCurrUId) return;
925 mainPane.msglistCurrUId = uid;
926 mainPane.threadListPositionDirty();
927 chiroSetMessageRead(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
928 setupTrayAnimation();
929 postScreenRebuild();
930 return;
933 })("next_unread", "move to next unread message; bool arg: can skip to next group?");
936 // //////////////////////////////////////////////////////////////////// //
937 conRegFunc!(() {
938 if (mainPane !is null) mainPane.scrollByPageUp();
939 })("artext_page_up", "do pageup on article text");
941 conRegFunc!(() {
942 if (mainPane !is null) mainPane.scrollByPageDown();
943 })("artext_page_down", "do pagedown on article text");
945 conRegFunc!(() {
946 if (mainPane !is null) mainPane.scrollBy(-1);
947 })("artext_line_up", "do lineup on article text");
949 conRegFunc!(() {
950 if (mainPane !is null) mainPane.scrollBy(1);
951 })("artext_line_down", "do linedown on article text");
953 // //////////////////////////////////////////////////////////////////// //
954 /*FIXME
955 conRegFunc!((bool forceOpera=false) {
956 if (auto fldx = getActiveFolder) {
957 fldx.withBaseReader((abase, cur, top, alist) {
958 if (cur < alist.length) {
959 abase.loadContent(alist[cur]);
960 if (auto art = abase[alist[cur]]) {
961 scope(exit) art.releaseContent();
962 auto path = art.getHeaderValue("path:");
963 //conwriteln("path: [", path, "]");
964 if (path.startsWithCI("digitalmars.com!.POSTED.")) {
965 import std.stdio : File;
966 import std.process;
967 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
968 string id = art.msgid;
969 if (id.length > 2 && id[0] == '<' && id[$-1] == '>') id = id[1..$-1];
970 auto pid = spawnProcess(
971 [getBrowserCommand(forceOpera), "http://forum.dlang.org/post/"~id],
972 File("/dev/zero"), File("/dev/null"), File("/dev/null"),
974 pid.wait();
980 })("article_open_in_browser", "open the current article in browser (dlang forum)");
983 /*FIXME
984 conRegFunc!(() {
985 if (auto fldx = getActiveFolder) {
986 fldx.withBaseReader((abase, cur, top, alist) {
987 if (cur < alist.length) {
988 if (auto art = abase[alist[cur]]) {
989 auto path = art.getHeaderValue("path:");
990 if (path.startsWithCI("digitalmars.com!.POSTED.")) {
991 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
992 string id = art.msgid;
993 if (id.length > 2 && id[0] == '<' && id[$-1] == '>') id = id[1..$-1];
994 id = "http://forum.dlang.org/post/"~id;
995 setClipboardText(vbwin, id);
996 setPrimarySelection(vbwin, id);
1002 })("article_copy_url_to_clipboard", "copy the url of the current article to clipboard (dlang forum)");
1006 // //////////////////////////////////////////////////////////////////// //
1007 conRegFunc!((ConString oldmail, ConString newmail, ConString newname) {
1008 if (oldmail.length) {
1009 if (newmail.length == 0) {
1010 repmailreps.remove(oldmail.idup);
1011 } else {
1012 MailReplacement mr;
1013 mr.oldmail = oldmail.idup;
1014 mr.newmail = newmail.idup;
1015 mr.newname = newname.idup;
1016 repmailreps[mr.oldmail] = mr;
1019 })("append_replyto_mail_replacement", "append replacement for reply mails");
1022 // //////////////////////////////////////////////////////////////////// //
1023 conRegFunc!(() {
1024 if (mainPane !is null && mainPane.threadListUp()) {
1025 postScreenRebuild();
1027 })("article_prev", "go to previous article");
1029 conRegFunc!(() {
1030 if (mainPane !is null && mainPane.threadListDown()) {
1031 postScreenRebuild();
1033 })("article_next", "go to next article");
1035 conRegFunc!(() {
1036 if (mainPane !is null && mainPane.threadListPageUp()) {
1037 postScreenRebuild();
1039 })("article_pgup", "artiles list: page up");
1041 conRegFunc!(() {
1042 if (mainPane !is null && mainPane.threadListPageDown()) {
1043 postScreenRebuild();
1045 })("article_pgdown", "artiles list: page down");
1047 conRegFunc!(() {
1048 if (mainPane !is null && mainPane.threadListScrollUp(movecurrent:false)) {
1049 postScreenRebuild();
1051 })("article_scroll_up", "scroll article list up");
1053 conRegFunc!(() {
1054 if (mainPane !is null && mainPane.threadListScrollDown(movecurrent:false)) {
1055 postScreenRebuild();
1057 })("article_scroll_down", "scroll article list up");
1059 conRegFunc!(() {
1060 if (mainPane !is null && mainPane.threadListHome()) {
1061 postScreenRebuild();
1063 })("article_to_first", "go to first article");
1065 conRegFunc!(() {
1066 if (mainPane !is null && mainPane.threadListEnd()) {
1067 postScreenRebuild();
1069 })("article_to_last", "go to last article");
1072 // //////////////////////////////////////////////////////////////////// //
1073 conRegFunc!(() {
1074 /*FIXME
1075 if (auto fld = getActiveFolder) {
1076 auto postDg = delegate (Account acc) {
1077 conwriteln("post with account '", acc.name, "' (", acc.mail, ")");
1078 auto pw = new PostWindow();
1079 pw.from.str = acc.realname~" <"~acc.mail~">";
1080 pw.from.readonly = true;
1081 if (auto nna = cast(NntpAccount)acc) {
1082 pw.title = "Reply to NNTP "~nna.server~":"~nna.group;
1083 pw.to.str = nna.group;
1084 pw.to.readonly = true;
1085 pw.activeWidget = pw.subj;
1086 } else {
1087 pw.title = "Reply from '"~acc.name~"' ("~acc.mail~")";
1088 pw.to.str = "";
1089 pw.activeWidget = pw.to;
1091 pw.subj.str = "";
1092 pw.acc = acc;
1093 pw.fld = fld;
1096 auto acc = fld.findAccountToPost();
1097 if (acc is null) {
1098 auto wacc = new SelectPopBoxWindow(defaultAcc);
1099 wacc.onSelected = postDg;
1100 } else {
1101 postDg(acc);
1102 acc = defaultAcc;
1104 } else {
1105 conwriteln("post: no active folder");
1108 })("new_post", "post a new article or message");
1111 // //////////////////////////////////////////////////////////////////// //
1112 static string buildToStr (string name, string mail, ref string newfromname) {
1113 if (auto mrp = mail in repmailreps) {
1114 newfromname = mrp.newname;
1115 return mrp.newname~" <"~mrp.newmail~">";
1116 } else {
1117 return name~" <"~mail~">";
1121 static void doReply (string repfld) {
1122 /*FIXME
1123 if (auto fld = getActiveFolder) {
1124 auto postDg = delegate (Account acc) {
1125 conwriteln("reply with account '", acc.name, "' (", acc.mail, ")");
1126 fld.withBaseReader((abase, cur, top, alist) {
1127 if (cur < alist.length) {
1128 auto aidx = alist.ptr[cur];
1129 abase.loadContent(aidx);
1130 if (auto art = abase[aidx]) {
1131 assert(art.contentLoaded);
1132 auto atext = art.getTextContent;
1133 auto pw = new PostWindow();
1134 pw.from.str = acc.realname~" <"~acc.mail~">";
1135 pw.from.readonly = true;
1136 string from = art.fromname;
1138 if (auto nna = cast(NntpAccount)acc) {
1139 pw.title = "Reply to NNTP "~nna.server~":"~nna.group;
1140 pw.to.str = nna.group;
1141 pw.to.readonly = true;
1142 } else {
1143 pw.title = "Reply from '"~acc.name~"' ("~acc.mail~")";
1144 //pw.to.str = art.fromname~" <"~art.frommail~">";
1145 string rs;
1146 if (repfld != "From") {
1147 auto rf = art.getHeaderValue(repfld);
1148 if (rf) {
1149 //rs = buildToStr(art.fromname, repfld, from);
1150 if (art.getHeaderValue("List-Id").length) {
1151 rs = rf.idup;
1152 } else {
1153 rs = art.fromname~" <"~rf.idup~">";
1157 if (!rs) rs = buildToStr(art.fromname, art.frommail, from);
1158 pw.to.str = rs;
1160 pw.subj.str = "Re: "~art.subj;
1163 auto vp = from.indexOf(" via Digitalmars-");
1164 if (vp > 0) {
1165 from = from[0..vp].xstrip;
1166 if (from.length == 0) from = "anonymous";
1170 pw.ed.addText(from);
1171 pw.ed.addText(" wrote:\n");
1172 pw.ed.addText("\n");
1173 foreach (ConString s; LineIterator!false(atext)) {
1174 if (s.length == 0 || s[0] == '>') pw.ed.addText(">"); else pw.ed.addText("> ");
1175 pw.ed.addText(s);
1176 pw.ed.addText("\n");
1178 pw.ed.addText("\n");
1179 pw.ed.reformat();
1180 pw.replyto = art.msgid;
1181 pw.references = art.getHeaderValue("References").xstrip.idup;
1182 pw.acc = acc;
1183 pw.fld = fld;
1184 pw.activeWidget = pw.ed;
1190 auto acc = fld.findAccountToPost(fld.curidx);
1191 if (acc is null) {
1192 auto wacc = new SelectPopBoxWindow(defaultAcc);
1193 wacc.onSelected = postDg;
1194 } else {
1195 postDg(acc);
1196 acc = defaultAcc;
1202 conRegFunc!(() { doReply("Reply-To"); })("article_reply", "reply to the current article with \"reply-to\" field");
1203 conRegFunc!(() { doReply("From"); })("article_reply_to_from", "reply to the current article with \"from\" field");
1206 // //////////////////////////////////////////////////////////////////// //
1207 conRegFunc!(() {
1208 if (vbwin is null || vbwin.closed) return;
1209 if (mainPane is null) return;
1210 if (mainPane.msglistCurrUId) {
1211 //messageBogoMarkHam(mainPane.msglistCurrUId);
1212 foreach (auto mrow; dbStore.statement(`SELECT ChiroUnpack(data) AS data FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", mainPane.msglistCurrUId).range) {
1213 try {
1214 import core.stdc.stdio : snprintf;
1215 char[128] fname = void;
1216 auto len = snprintf(fname.ptr, fname.length, "~/Mail/_bots/z_eml_%u.eml", cast(uint)mainPane.msglistCurrUId);
1217 auto fo = VFile(fname[0..len], "w");
1218 fo.writeln(mrow.data!SQ3Text.xstripright);
1219 fo.close();
1220 conwriteln("article exported to: ", fname[0..len]);
1221 } catch (Exception e) {
1225 })("article_export", "export current article as raw text");
1228 conRegFunc!(() {
1229 if (vbwin is null || vbwin.closed) return;
1230 if (mainPane is null) return;
1231 if (mainPane.msglistCurrUId) {
1232 messageBogoMarkHam(mainPane.msglistCurrUId);
1233 //TODO: move out of spam
1235 })("article_mark_ham", "mark current article as ham");
1238 conRegFunc!(() {
1239 if (vbwin is null || vbwin.closed) return;
1240 if (mainPane is null) return;
1241 if (mainPane.msglistCurrUId) {
1242 immutable uint uid = mainPane.msglistCurrUId;
1243 // move to the next message
1244 if (!mainPane.threadListDown()) mainPane.threadListUp();
1245 conwriteln("calling bogofilter...");
1246 messageBogoMarkSpam(uid);
1247 conwriteln("adding '#spam' tag");
1248 immutable bool updatePane = chiroMessageAddTag(uid, "#spam");
1249 conwriteln("removing other virtual folder tags...");
1250 DynStr[] tags;
1251 scope(exit) delete tags;
1252 tags.reserve(32);
1253 static auto stGetMsgTags = LazyStatement!"View"(`
1254 SELECT DISTINCT(tagid) AS tagid, tt.tag AS name
1255 FROM threads
1256 INNER JOIN tagnames AS tt USING(tagid)
1257 WHERE uid=:uid
1258 ;`);
1259 foreach (auto row; stGetMsgTags.st.bind(":uid", uid).range) {
1260 auto tag = row.name!SQ3Text;
1261 conwriteln(" tag: ", tag);
1262 if (tag.startsWith("/")) tags ~= DynStr(tag);
1264 foreach (ref DynStr tn; tags) {
1265 conwriteln("removing tag '", tn.getData, "'...");
1266 chiroMessageRemoveTag(uid, tn.getData);
1268 conwriteln("done marking as spam.");
1269 if (updatePane) {
1270 mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
1271 } else {
1272 // rescan folder
1273 mainPane.updateViewsIfTid(chiroGetTagUid("#spam"));
1275 postScreenRebuild();
1277 })("article_mark_spam", "mark current article as spam");
1280 // //////////////////////////////////////////////////////////////////// //
1281 conRegFunc!((ConString fldname) {
1282 if (vbwin is null || vbwin.closed) return;
1283 if (mainPane is null) return;
1284 if (!mainPane.msglistCurrUId) return;
1285 immutable uint uid = mainPane.msglistCurrUId;
1287 fldname = fldname.xstrip;
1288 while (fldname.length && fldname[$-1] == '/') fldname = fldname[0..$-1].xstrip;
1289 immutable bool isforced = (fldname.length && fldname[0] == '!');
1290 if (isforced) fldname = fldname[1..$];
1291 if (fldname.length == 0) {
1292 conwriteln("ERROR: cannot move to empty folder");
1293 return;
1295 if (fldname[0].isalnum) {
1296 conwriteln("ERROR: invalid folder name '", fldname, "'");
1297 return;
1300 uint tagid;
1301 if (!isforced) {
1302 tagid = chiroGetTagUid(fldname);
1303 } else {
1304 tagid = chiroAppendTag(fldname, hidden:(fldname[0] == '#' ? 1 : 0));
1306 if (!tagid) {
1307 conwriteln("ERROR: invalid folder name '", fldname, "'");
1308 return;
1311 immutable bool updatePane = chiroMessageAddTag(uid, fldname);
1312 DynStr[] tags;
1313 scope(exit) delete tags;
1314 tags.reserve(32);
1315 static auto stGetMsgTags = LazyStatement!"View"(`
1316 SELECT DISTINCT(tagid) AS tagid, tt.tag AS name
1317 FROM threads
1318 INNER JOIN tagnames AS tt USING(tagid)
1319 WHERE uid=:uid
1320 ;`);
1321 foreach (auto row; stGetMsgTags.st.bind(":uid", uid).range) {
1322 auto tag = row.name!SQ3Text;
1323 conwriteln(" tag: ", tag);
1324 if (tag.startsWith("account:")) continue;
1325 if (tag != fldname) tags ~= DynStr(tag);
1327 foreach (ref DynStr tn; tags) {
1328 conwriteln("removing tag '", tn.getData, "'...");
1329 chiroMessageRemoveTag(uid, tn.getData);
1331 if (updatePane) {
1332 mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
1333 } else {
1334 // rescan folder
1335 mainPane.updateViewsIfTid(chiroGetTagUid("#spam"));
1337 postScreenRebuild();
1338 })("article_move_to_folder", "move article to existing folder");
1341 // //////////////////////////////////////////////////////////////////// //
1342 version(none) {
1343 conRegFunc!(() {
1344 /*FIXME
1345 if (auto fld = getActiveFolder) {
1346 fld.moveToParent();
1347 postScreenRebuild();
1350 })("article_to_parent", "jump to parent article, if any");
1352 conRegFunc!(() {
1353 /*FIXME
1354 if (auto fld = getActiveFolder) {
1355 fld.moveToPrevSib();
1356 postScreenRebuild();
1359 })("article_to_prev_sib", "jump to previous sibling");
1361 conRegFunc!(() {
1362 /*FIXME
1363 if (auto fld = getActiveFolder) {
1364 fld.moveToNextSib();
1365 postScreenRebuild();
1368 })("article_to_next_sib", "jump to next sibling");
1371 // //////////////////////////////////////////////////////////////////// //
1372 conRegFunc!(() {
1373 if (vbwin is null || vbwin.closed) return;
1374 if (mainPane is null) return;
1375 if (mainPane.lastDecodedTid == 0 || mainPane.msglistCurrUId == 0) return;
1376 DynStr tagname = chiroGetTagName(mainPane.lastDecodedTid);
1377 if (!globmatch(tagname.getData, "/dmars_ng/*")) return;
1378 // get from
1379 DynStr fromMail, fromName;
1380 if (!chiroGetMessageFrom(mainPane.msglistCurrUId, ref fromMail, ref fromName)) return;
1381 if (fromMail.length == 0 && fromName.length == 0) return;
1382 //writeln("!!! email=", fromMail.getData, "; name=", fromName.getData, "|");
1384 uint twitid = 0;
1385 DynStr twtitle;
1386 DynStr twname;
1387 DynStr twemail;
1388 DynStr twnotes;
1389 DynStr twtagglob = "/dmars_ng/*";
1390 bool withName = false;
1392 static auto stFindTwit = LazyStatement!"Conf"(`
1393 SELECT
1394 etwitid AS twitid
1395 , tagglob AS tagglob
1396 , email AS email
1397 , name AS name
1398 , title AS title
1399 , notes AS notes
1400 FROM emailtwits
1401 WHERE email=:email AND name=:name
1402 ;`);
1404 static auto stAddTwit = LazyStatement!"Conf"(`
1405 INSERT INTO emailtwits
1406 ( tagglob, email, name, title, notes)
1407 VALUES(:tagglob,:email,:name,:title,:notes)
1408 ;`);
1410 static auto stModifyTwit = LazyStatement!"Conf"(`
1411 UPDATE emailtwits
1413 tagglob=:tagglob
1414 , email=:email
1415 , name=:name
1416 , title=:title
1417 , notes=:notes
1418 WHERE etwitid=:twitid
1419 ;`);
1421 static auto stRemoveTwitAuto = LazyStatement!"Conf"(`
1422 DELETE FROM msgidtwits
1423 WHERE etwitid=:twitid
1424 ;`);
1426 static auto stRemoveTwit = LazyStatement!"Conf"(`
1427 DELETE FROM emailtwits
1428 WHERE etwitid=:twitid
1429 ;`);
1431 stFindTwit.st
1432 .bindConstText(":email", fromMail.getData)
1433 .bindConstText(":name", fromName.getData);
1434 foreach (auto row; stFindTwit.st.range) {
1435 //conwriteln("!!!", row.twitid!uint, "; mail=", row.email!SQ3Text, "; name=", row.name!SQ3Text, "; title=", row.title!SQ3Text.recodeToKOI8);
1436 if (!globmatch(tagname.getData, row.tagglob!SQ3Text)) continue;
1437 twitid = row.twitid!uint;
1438 twtitle = row.title!SQ3Text;
1439 twname = row.name!SQ3Text;
1440 twemail = row.email!SQ3Text;
1441 twtagglob = row.tagglob!SQ3Text;
1442 twnotes = row.notes!SQ3Text;
1443 withName = (fromName.length != 0);
1444 break;
1447 if (!twitid && fromName.length) {
1448 stFindTwit.st
1449 .bindConstText(":email", fromMail.getData)
1450 .bindConstText(":name", "");
1451 foreach (auto row; stFindTwit.st.range) {
1452 //conwriteln("!!!", row.twitid!uint, "; mail=", row.email!SQ3Text, "; name=", row.name!SQ3Text, "; title=", row.title!SQ3Text.recodeToKOI8);
1453 if (!globmatch(tagname.getData, row.tagglob!SQ3Text)) continue;
1454 twitid = row.twitid!uint;
1455 twtitle = row.title!SQ3Text;
1456 twname = row.name!SQ3Text;
1457 twemail = row.email!SQ3Text;
1458 twtagglob = row.tagglob!SQ3Text;
1459 twnotes = row.notes!SQ3Text;
1460 withName = false;
1461 break;
1465 //conwriteln("twitid: ", twitid, "; title=", twtitle.getData.recodeToKOI8);
1467 auto tw = new TitlerWindow((twitid ? twname : fromName), (twitid ? twemail : fromMail), twtagglob, twtitle);
1468 tw.onSelected = delegate (name, email, glob, title) {
1469 if (email.length == 0 && name.length == 0) return false;
1470 if (glob.length == 0) return false;
1471 if (email.length && email[0] == '@') return false;
1472 title = title.xstrip;
1473 if (twitid) {
1474 if (title.length == 0) {
1475 // remove twit
1476 conwriteln("removing twit...");
1477 stRemoveTwitAuto.st.bind(":twitid", twitid).doAll();
1478 stRemoveTwit.st.bind(":twitid", twitid).doAll();
1479 } else {
1480 // change twit
1481 conwriteln("changing twit...");
1482 stModifyTwit.st
1483 .bind(":twitid", twitid)
1484 .bindConstText(":tagglob", glob)
1485 .bindConstText(":email", email)
1486 .bindConstText(":name", name)
1487 .bindConstText(":title", title)
1488 .bindConstText(":notes", twnotes.getData, allowNull:true)
1489 .doAll();
1491 } else {
1492 if (title.length == 0) return false;
1493 // new twit
1494 conwriteln("adding twit...");
1495 stAddTwit.st
1496 .bindConstText(":tagglob", glob)
1497 .bindConstText(":email", email)
1498 .bindConstText(":name", name)
1499 .bindConstText(":title", title)
1500 .bindConstText(":notes", null, allowNull:true)
1501 .doAll();
1504 if (vbwin && !vbwin.closed) vbwin.postEvent(new RecalcAllTwitsEvent());
1505 return true;
1507 })("article_edit_poster_title", "edit poster's title of the current article");
1510 // //////////////////////////////////////////////////////////////////// //
1511 conRegFunc!(() {
1512 if (vbwin is null || vbwin.closed) return;
1513 if (mainPane is null) return;
1514 if (mainPane.lastDecodedTid == 0 || mainPane.msglistCurrUId == 0) return;
1515 DynStr tagname = chiroGetTagName(mainPane.lastDecodedTid);
1516 if (!globmatch(tagname.getData, "/dmars_ng/*")) return;
1517 int mute = chiroGetMessageMute(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
1518 // check for non-automatic mutes
1519 if (mute != Mute.Normal && mute <= Mute.ThreadStart) return;
1520 createTwitByMsgid(mainPane.msglistCurrUId);
1521 mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
1522 postScreenRebuild();
1523 })("article_twit_thread", "twit current thread");
1525 conRegFunc!(() {
1526 if (vbwin is null || vbwin.closed) return;
1527 if (mainPane is null) return;
1528 int app = chiroGetMessageAppearance(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
1529 if (app >= 0) {
1530 //conwriteln("oldapp: ", app, " (", isSoftDeleted(app), ")");
1531 immutable bool wasPurged = (app == Appearance.SoftDeletePurge);
1532 app =
1533 app == Appearance.SoftDeletePurge ? Appearance.SoftDeleteUser :
1534 isSoftDeleted(app) ? Appearance.Read :
1535 Appearance.SoftDeleteUser;
1536 //conwriteln("newapp: ", app);
1537 chiroSetMessageAppearance(mainPane.lastDecodedTid, mainPane.msglistCurrUId, cast(Appearance)app);
1538 if (!wasPurged && isSoftDeleted(app)) mainPane.threadListDown();
1539 postScreenRebuild();
1541 })("article_softdelete_toggle", "toggle \"soft deleted\" flag on current article");
1543 conRegFunc!(() {
1544 if (vbwin is null || vbwin.closed) return;
1545 if (mainPane is null) return;
1546 int app = chiroGetMessageAppearance(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
1547 if (app >= 0) {
1548 //conwriteln("oldapp: ", app);
1549 app =
1550 app == Appearance.SoftDeletePurge ? Appearance.Read :
1551 isSoftDeleted(app) ? Appearance.SoftDeletePurge :
1552 Appearance.SoftDeletePurge;
1553 //conwriteln("newapp: ", app);
1554 chiroSetMessageAppearance(mainPane.lastDecodedTid, mainPane.msglistCurrUId, cast(Appearance)app);
1555 if (app == Appearance.SoftDeletePurge) mainPane.threadListDown();
1556 postScreenRebuild();
1558 })("article_harddelete_toggle", "toggle \"hard deleted\" flag on current article");
1561 // //////////////////////////////////////////////////////////////////// //
1562 conRegFunc!(() {
1563 if (vbwin is null || vbwin.closed) return;
1564 if (mainPane is null) return;
1565 DynStr hdrs = chiroMessageHeaders(mainPane.msglistCurrUId);
1566 if (hdrs.length == 0) return;
1567 conwriteln("============================");
1568 conwriteln("message uid: ", mainPane.msglistCurrUId);
1569 conwriteln(" tag tagid: ", mainPane.lastDecodedTid);
1570 conwriteln("============================");
1571 forEachHeaderLine(hdrs.getData, (const(char)[] line) {
1572 conwriteln(" ", line.xstripright);
1573 return true; // go on
1575 })("article_dump_headers", "dump article headers");
1577 conRegFunc!(() {
1578 if (vbwin is null || vbwin.closed) return;
1579 if (mainPane is null) return;
1580 DynStr hdrs = chiroMessageHeaders(mainPane.msglistCurrUId);
1581 if (hdrs.length == 0) return;
1582 conwriteln("============================");
1583 conwriteln("message uid: ", mainPane.msglistCurrUId);
1584 conwriteln(" tag tagid: ", mainPane.lastDecodedTid);
1585 conwriteln("============================");
1586 auto bogo = messageBogoCheck(mainPane.msglistCurrUId);
1587 conwriteln("BOGO RESULT: ", bogo);
1588 })("article_bogo_check", "check article with bogofilter (purely informational)");
1590 conRegFunc!(() {
1591 if (mainPane !is null && mainPane.lastDecodedTid) {
1592 conwriteln("relinking ", mainPane.lastDecodedTid, " (", chiroGetTagName(mainPane.lastDecodedTid).getData, ")...");
1593 chiroSupportRelinkTagThreads(mainPane.lastDecodedTid);
1594 mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
1595 postScreenRebuild();
1597 })("folder_rebuild_index", "rebuild thread index");
1599 conRegFunc!(() {
1600 if (vbwin && !vbwin.closed && mainPane !is null /*&& mainPane.lastDecodedTid*/) {
1601 vbwin.postEvent(new RecalcAllTwitsEvent());
1603 })("folder_rebuild_twits", "rebuild all twits");
1605 conRegFunc!(() {
1606 if (mainPane !is null && mainPane.folderCurrTag.length) {
1607 auto w = new TagOptionsWindow(mainPane.folderCurrTag.getData);
1608 w.onUpdated = delegate void (tagname) {
1609 if (vbwin && !vbwin.closed && mainPane !is null && mainPane.lastDecodedTagName == tagname) {
1610 mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
1613 postScreenRebuild();
1615 })("folder_options", "show options window");
1618 conRegFunc!(() {
1619 if (auto fld = getActiveFolder) {
1620 fld.withBase(delegate (abase) {
1621 uint idx = fld.curidx;
1622 if (idx >= fld.length) {
1623 idx = 0;
1624 } else if (auto art = abase[fld.baseidx(idx)]) {
1625 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
1626 idx = (idx+1)%fld.length;
1629 foreach (immutable _; 0..fld.length) {
1630 auto art = abase[fld.baseidx(idx)];
1631 if (art !is null) {
1632 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
1633 fld.curidx = cast(int)idx;
1634 postScreenRebuild();
1635 return;
1638 idx = (idx+1)%fld.length;
1642 })("find_mine", "find mine article");
1645 // //////////////////////////////////////////////////////////////////// //
1646 conRegVar!dbg_dump_keynames("dbg_dump_key_names", "dump key names");
1649 // //////////////////////////////////////////////////////////////////// //
1650 conRegFunc!((uint idx) {
1651 if (vbwin is null || vbwin.closed) return;
1652 if (mainPane is null) return;
1653 if (mainPane.msglistCurrUId == 0) return;
1655 static auto statAttaches = LazyStatement!"View"(`
1656 SELECT
1657 idx AS idx
1658 , mime AS mime
1659 , name AS name
1660 , ChiroUnpack(content) AS content
1661 FROM attaches
1662 WHERE uid=:uid
1663 ORDER BY idx
1664 ;`);
1665 char[128] buf = void;
1666 uint attidx = 0;
1667 foreach (auto row; statAttaches.st.bind(":uid", mainPane.msglistCurrUId).range) {
1668 import core.stdc.stdio : snprintf;
1669 if (idx != 0) { --idx; ++attidx; continue; }
1670 if (!row.mime!SQ3Text.startsWith("image")) return;
1671 auto name = row.name!SQ3Text.getExtension;
1672 if (name.length == 0) return;
1673 try {
1674 DynStr fname;
1675 fname = "/tmp/_viewimage_";
1677 import std.uuid;
1678 UUID id = randomUUID();
1679 foreach (immutable ubyte b; id.data[]) {
1680 fname ~= "0123456789abcdef"[b>>4];
1681 fname ~= "0123456789abcdef"[b&0x0f];
1683 fname ~= name;
1685 conwriteln("writing attach #", attidx, " to '", fname.getData, "'");
1686 VFile fo = VFile(fname.getData, "w");
1687 fo.rawWriteExact(row.content!SQ3Text);
1688 fo.close();
1690 auto vtid = spawn(&imageViewThread, thisTid);
1691 string fnamestr = fname.getData.idup;
1692 vtid.send(ImgViewCommand(fnamestr));
1693 } catch (Exception e) {
1694 conwriteln("ERROR writing attachment: ", e.msg);
1696 break;
1698 })("attach_view", "view attached image: attach_view index");
1701 // //////////////////////////////////////////////////////////////////// //
1702 conRegFunc!((uint idx, ConString userfname=null) {
1703 if (vbwin is null || vbwin.closed) return;
1704 if (mainPane is null) return;
1705 if (mainPane.msglistCurrUId == 0) return;
1707 static auto statAttaches = LazyStatement!"View"(`
1708 SELECT
1709 idx AS idx
1710 , mime AS mime
1711 , name AS name
1712 , ChiroUnpack(content) AS content
1713 FROM attaches
1714 WHERE uid=:uid
1715 ORDER BY idx
1716 ;`);
1717 char[128] buf = void;
1718 uint attidx = 0;
1719 foreach (auto row; statAttaches.st.bind(":uid", mainPane.msglistCurrUId).range) {
1720 import core.stdc.stdio : snprintf;
1721 if (idx != 0) { --idx; ++attidx; continue; }
1722 try {
1723 auto name = row.name!SQ3Text/*.decodeSubj*/;
1724 DynStr fname;
1726 if (userfname.length) {
1727 fname = userfname;
1728 } else {
1729 fname = "/tmp/";
1730 name = name.sanitizeFileNameStr;
1731 if (name.length == 0) {
1732 auto len = snprintf(buf.ptr, buf.sizeof, "attach_%02u.bin", attidx);
1733 fname ~= buf[0..cast(usize)len];
1734 } else {
1735 fname ~= name;
1739 conwriteln("writing attach #", attidx, " to '", fname.getData, "'");
1740 VFile fo = VFile(fname.getData, "w");
1741 fo.rawWriteExact(row.content!SQ3Text);
1742 fo.close();
1743 } catch (Exception e) {
1744 conwriteln("ERROR writing attachment: ", e.msg);
1746 break;
1748 })("attach_save", "save attach: attach_save index [filename]");
1752 // ////////////////////////////////////////////////////////////////////////// //
1753 //FIXME: turn into property
1754 final class ArticleTextScrollEvent {}
1755 __gshared ArticleTextScrollEvent evArticleScroll;
1756 shared static this () {
1757 evArticleScroll = new ArticleTextScrollEvent();
1760 void postArticleScroll () {
1761 if (vbwin !is null && !vbwin.eventQueued!ArticleTextScrollEvent) vbwin.postTimeout(evArticleScroll, 25);
1765 // ////////////////////////////////////////////////////////////////////////// //
1766 class MarkAsUnreadEvent {
1767 uint tagid;
1768 uint uid;
1771 __gshared int unreadTimeoutInterval = 600;
1772 __gshared int scrollKeepLines = 3;
1773 __gshared bool preferHtmlContent = false;
1774 __gshared bool detectHtmlContent = true;
1777 void postMarkAsUnreadEvent () {
1778 if (unreadTimeoutInterval > 0 && unreadTimeoutInterval < 10000 && vbwin !is null && mainPane !is null) {
1779 if (!mainPane.msglistCurrUId) return;
1780 if (chiroGetMessageUnread(mainPane.lastDecodedTid, mainPane.msglistCurrUId)) {
1781 //conwriteln("setting new unread timer");
1782 auto evt = new MarkAsUnreadEvent();
1783 evt.tagid = mainPane.lastDecodedTid;
1784 evt.uid = mainPane.msglistCurrUId;
1785 vbwin.postTimeout(evt, unreadTimeoutInterval);
1791 // ////////////////////////////////////////////////////////////////////////// //
1792 __gshared MainPaneWindow mainPane;
1795 final class MainPaneWindow : SubWindow {
1796 string[] emlines; // in utf
1797 uint emlinesBeforeAttaches;
1798 uint lastDecodedUId;
1799 uint lastDecodedTid; // for which folder we last build our view?
1800 uint lastMaxTidUid;
1801 DynStr lastDecodedTagName;
1802 DynStr arttoname;
1803 DynStr arttomail;
1804 DynStr artfromname;
1805 DynStr artfrommail;
1806 DynStr artsubj;
1807 DynStr arttime;
1808 int articleTextTopLine = 0;
1809 int articleDestTextTopLine = 0;
1810 //__gshared Timer unreadTimer; // as main pane is never destroyed, there's no need to kill the timer
1811 //__gshared Folder unreadFolder;
1812 //__gshared uint unreadIdx;
1813 int linesInHeader;
1814 // folder view
1815 DynStr folderTopTag = null;
1816 DynStr folderCurrTag = null;
1817 int folderTopIndex = 0;
1818 int folderCurrIndex = 0;
1819 // message list view
1820 uint msglistTopUId = 0;
1821 uint msglistCurrUId = 0;
1822 bool saveMsgListPositions = false;
1823 MonoTime lastStateSaveTime;
1824 uint viewDataVersion;
1826 this () {
1827 super(null, GxSize(screenWidth, screenHeight));
1828 allowMinimise = false;
1829 allowDragMove = false;
1830 mType = Type.OnBottom;
1831 add();
1832 loadSavedState();
1833 lastStateSaveTime = MonoTime.currTime;
1834 viewDataVersion = dbView.getDataVersion()-1u;
1837 bool isViewChanged () const nothrow @trusted @nogc {
1838 pragma(inline, true);
1839 return (viewDataVersion != dbView.getDataVersion());
1842 void updateViewsIfTid (uint tid) {
1843 if (tid && lastDecodedTid == tid) {
1844 switchToFolderTid(lastDecodedTid, forced:true);
1848 void checkSaveState () {
1849 if (lastStateSaveTime+dur!"seconds"(30) <= MonoTime.currTime) {
1850 //writeln("saving state...");
1851 saveCurrentState();
1852 lastStateSaveTime = MonoTime.currTime;
1853 if (lastDecodedTid) {
1854 immutable maxuid = chiroGetTreePaneTableMaxUId();
1855 if (maxuid && maxuid > lastMaxTidUid) {
1856 // something was changed in the database, rescal
1857 updateViewsIfTid(lastDecodedTid);
1863 void saveCurrentState () {
1864 transacted!"Conf"{
1865 chiroSetOption("/mainpane/folders/toptid", folderTopTag);
1866 chiroSetOption("/mainpane/folders/currtid", folderCurrTag);
1867 saveThreadListPositions(transaction:false);
1871 void loadSavedState () {
1872 chiroGetOption(folderTopTag, "/mainpane/folders/toptid");
1873 chiroGetOption(folderCurrTag, "/mainpane/folders/currtid");
1874 //conwriteln("curr: ", folderCurrTag);
1875 //conwriteln(" top: ", folderTopTag);
1876 rescanFolders(forced:true);
1880 private void threadListPositionDirty () nothrow @safe @nogc {
1881 saveMsgListPositions = true;
1884 private void saveThreadListPositionsInternal () {
1885 if (lastDecodedTagName.length == 0) return;
1886 import core.stdc.stdio : snprintf;
1887 const(char)[] tn = lastDecodedTagName.getData;
1888 char[128] xname = void;
1889 auto xlen = snprintf(xname.ptr, xname.sizeof, "/mainpane/msgview/current%.*s", cast(uint)tn.length, tn.ptr);
1890 chiroSetOptionUInts(xname[0..xlen], msglistTopUId, msglistCurrUId);
1893 private void saveThreadListPositions (immutable bool transaction=true) {
1894 if (!saveMsgListPositions) return;
1895 saveMsgListPositions = false;
1896 if (lastDecodedTid == 0) return;
1897 if (transaction) {
1898 transacted!"Conf"(&saveThreadListPositionsInternal);
1899 } else {
1900 saveThreadListPositionsInternal();
1904 private void loadThreadListPositions () {
1905 saveMsgListPositions = false;
1906 msglistTopUId = msglistCurrUId = 0;
1907 if (lastDecodedTid == 0) return;
1908 import core.stdc.stdio : snprintf;
1909 char[128] xname = void;
1910 const(char)[] tn = lastDecodedTagName.getData;
1911 auto xlen = snprintf(xname.ptr, xname.sizeof, "/mainpane/msgview/current%.*s", cast(uint)tn.length, tn.ptr);
1912 chiroGetOptionUInts(ref msglistTopUId, ref msglistCurrUId, xname[0..xlen]);
1913 setupUnreadTimer();
1917 private int getFolderMonthLimit () {
1918 version(none) {
1919 import core.stdc.stdio : snprintf;
1920 char[1024] xname = void;
1921 if (lastDecodedTid == 0) return chiroGetOption!int("/mainpane/msgview/monthlimit", 6);
1922 bool exists;
1923 const(char)[] tn = lastDecodedTagName.getData;
1924 for (;;) {
1925 auto len = snprintf(xname.ptr, xname.sizeof, "/mainpane/msgview/monthlimit%.*s", cast(uint)tn.length, tn.ptr);
1926 int v = chiroGetOptionEx!int(xname[0..len], out exists);
1927 //writeln(tn, " :: ", v, " :: ", exists);
1928 if (exists) return v;
1929 auto slp = tn.lastIndexOf('/', 1);
1930 if (slp <= 0) break;
1931 tn = tn[0..slp];
1933 return chiroGetOption!int("/mainpane/msgview/monthlimit", 6);
1934 } else {
1935 if (lastDecodedTid == 0) return chiroGetOption!int("/mainpane/msgview/monthlimit", 6);
1936 return chiroGetTagMonthLimit(lastDecodedTagName.getData, 6);
1941 void rescanFolders (bool forced=false) {
1942 if (!.rescanFolders()) { if (!forced) return; }
1943 folderTopIndex = folderCurrIndex = 0;
1944 bool foundTop = false, foundCurr = false;
1945 foreach (immutable idx, const FolderInfo fi; folderList) {
1946 if (!foundTop && fi.name == folderTopTag) { foundTop = true; folderTopIndex = cast(int)idx; }
1947 if (!foundCurr && fi.name == folderCurrTag) { foundCurr = true; folderCurrIndex = cast(int)idx; }
1949 if (!foundTop) folderTopTag.clear();
1950 if (!foundCurr) folderCurrTag.clear();
1953 void folderMakeCurVisible () {
1954 rescanFolders();
1955 if (folderList.length) {
1956 if (folderCurrIndex >= folderList.length) folderCurrIndex = cast(uint)folderList.length-1;
1957 if (folderTopIndex >= folderList.length) folderTopIndex = cast(uint)folderList.length-1;
1958 if (folderCurrIndex-3 < folderTopIndex) {
1959 folderTopIndex = folderCurrIndex-3;
1960 if (folderTopIndex < 0) folderTopIndex = 0;
1962 int hgt = screenHeight/(gxTextHeightUtf+2)-1;
1963 if (hgt < 1) hgt = 1;
1964 if (folderCurrIndex+2 > folderTopIndex+hgt) {
1965 folderTopIndex = (folderCurrIndex+2 > hgt ? folderCurrIndex+2-hgt : 0);
1966 if (folderTopIndex < 0) folderTopIndex = 0;
1968 if (folderTopTag != folderList[folderTopIndex].name) folderTopTag = folderList[folderTopIndex].name;
1969 if (folderCurrTag != folderList[folderCurrIndex].name) folderCurrTag = folderList[folderCurrIndex].name;
1970 } else {
1971 folderCurrIndex = folderTopIndex = 0;
1975 private void switchToFolderTid (const uint tid, bool forced=false) {
1976 //setupTrayAnimation();
1977 if (!forced && tid == lastDecodedTid) return;
1978 saveThreadListPositions();
1979 if (lastDecodedTid != 0) chiroDeletePurgedWithTag(lastDecodedTid);
1980 // rescan
1981 lastDecodedTid = tid;
1982 if (tid != 0) {
1983 lastDecodedTagName = chiroGetTagName(tid);
1984 immutable int monthlimit = getFolderMonthLimit();
1985 chiroCreateTreePaneTable(tid, lastmonthes:monthlimit);
1986 // load position
1987 loadThreadListPositions();
1988 // top
1989 if (!chiroIsTreePaneTableUidValid(msglistTopUId)) {
1990 msglistTopUId = chiroGetTreePaneTableIndex2Uid(0);
1991 threadListPositionDirty();
1993 // current
1994 if (!chiroIsTreePaneTableUidValid(msglistCurrUId)) {
1995 msglistCurrUId = chiroGetTreePaneTableIndex2Uid(0);
1996 threadListPositionDirty();
1998 lastMaxTidUid = chiroGetTreePaneTableMaxUId();
1999 setupUnreadTimer();
2000 } else {
2001 lastDecodedTagName.clear();
2002 msglistTopUId = 0;
2003 msglistCurrUId = 0;
2004 chiroClearTreePaneTable();
2008 void folderSetToIndex (int idx) {
2009 if (idx < 0 || idx >= folderList.length) return;
2010 if (idx == folderCurrIndex) return;
2011 folderCurrIndex = idx;
2012 folderCurrTag = folderList[folderCurrIndex].name;
2013 switchToFolderTid(folderList[folderCurrIndex].tagid);
2016 bool folderUpOne () {
2017 if (folderList.length == 0) return false;
2018 if (folderCurrIndex <= 0) return false;
2019 --folderCurrIndex;
2020 folderCurrTag = folderList[folderCurrIndex].name;
2021 setupUnreadTimer();
2022 return true;
2025 bool folderDownOne () {
2026 if (folderList.length == 0) return false;
2027 if (folderCurrIndex+1 >= cast(int)folderList.length) return false;
2028 ++folderCurrIndex;
2029 folderCurrTag = folderList[folderCurrIndex].name;
2030 setupUnreadTimer();
2031 return true;
2034 bool threadListHome () {
2035 if (lastDecodedTid == 0) return false;
2036 immutable uint firstUid = chiroGetTreePaneTableFirstUid();
2037 if (!firstUid || firstUid == msglistCurrUId) return false;
2038 msglistCurrUId = firstUid;
2039 threadListPositionDirty();
2040 setupUnreadTimer();
2041 return true;
2044 bool threadListEnd () {
2045 if (lastDecodedTid == 0) return false;
2046 immutable uint lastUid = chiroGetTreePaneTableLastUid();
2047 if (!lastUid || lastUid == msglistCurrUId) return false;
2048 msglistCurrUId = lastUid;
2049 threadListPositionDirty();
2050 setupUnreadTimer();
2051 return true;
2054 bool threadListUp () {
2055 import iv.timer;
2056 if (lastDecodedTid == 0) return false;
2057 auto ctm = Timer(true);
2058 immutable uint prevUid = chiroGetTreePaneTablePrevUid(msglistCurrUId);
2059 ctm.stop;
2060 if (ChiroTimerExEnabled) writeln("threadListUp time: ", ctm);
2061 if (!prevUid || prevUid == msglistCurrUId) return false;
2062 msglistCurrUId = prevUid;
2063 threadListPositionDirty();
2064 setupUnreadTimer();
2065 return true;
2068 bool threadListDown () {
2069 import iv.timer;
2070 if (lastDecodedTid == 0) return false;
2071 auto ctm = Timer(true);
2072 immutable uint nextUid = chiroGetTreePaneTableNextUid(msglistCurrUId);
2073 ctm.stop;
2074 if (ChiroTimerExEnabled) writeln("threadListDown time: ", ctm);
2075 if (!nextUid || nextUid == msglistCurrUId) return false;
2076 msglistCurrUId = nextUid;
2077 threadListPositionDirty();
2078 setupUnreadTimer();
2079 return true;
2082 bool threadListScrollUp (bool movecurrent) {
2083 import iv.timer;
2084 if (lastDecodedTid == 0) return false;
2085 auto ctm = Timer(true);
2086 immutable uint topPrevUid = chiroGetTreePaneTablePrevUid(msglistTopUId);
2087 ctm.stop;
2088 if (ChiroTimerExEnabled) conwriteln("threadListScrollUp: prevtop time: ", ctm);
2089 if (!topPrevUid || topPrevUid == msglistTopUId) return false;
2090 ctm.restart;
2091 immutable uint currPrevUid = (movecurrent ? chiroGetTreePaneTablePrevUid(msglistCurrUId) : 0);
2092 ctm.stop;
2093 if (movecurrent && ChiroTimerExEnabled) conwriteln("threadListScrollUp: prevcurr time: ", ctm);
2094 if (movecurrent && !currPrevUid) return false;
2095 msglistTopUId = topPrevUid;
2096 if (movecurrent) {
2097 msglistCurrUId = currPrevUid;
2098 setupUnreadTimer();
2100 threadListPositionDirty();
2101 return true;
2104 bool threadListScrollDown (bool movecurrent) {
2105 import iv.timer;
2106 if (lastDecodedTid == 0) return false;
2107 auto ctm = Timer(true);
2108 immutable uint currNextUid = (movecurrent ? chiroGetTreePaneTableNextUid(msglistCurrUId) : 0);
2109 ctm.stop;
2110 if (movecurrent && ChiroTimerExEnabled) writeln("threadListScrollDown: nextcurr time: ", ctm);
2111 if (movecurrent && (!currNextUid || currNextUid == msglistCurrUId)) return false;
2112 ctm.restart;
2113 immutable uint topNextUid = chiroGetTreePaneTableNextUid(msglistTopUId);
2114 ctm.stop;
2115 if (ChiroTimerExEnabled) writeln("threadListScrollDown: nexttop time: ", ctm);
2116 if (!topNextUid) return false;
2117 msglistTopUId = topNextUid;
2118 if (movecurrent) {
2119 msglistCurrUId = currNextUid;
2120 setupUnreadTimer();
2122 threadListPositionDirty();
2123 return true;
2126 bool threadListPageUp () {
2127 import iv.timer;
2128 bool res = false;
2129 int hgt = guiThreadListHeight/gxTextHeightUtf-1;
2130 if (hgt < 1) hgt = 1;
2131 auto ctm = Timer(true);
2132 foreach (; 0..hgt) {
2133 if (!threadListScrollUp(movecurrent:true)) break;
2134 res = true;
2136 ctm.stop;
2137 if (ChiroTimerExEnabled) writeln("threadListPageUp time: ", ctm);
2138 if (res) setupUnreadTimer();
2139 return res;
2142 bool threadListPageDown () {
2143 import iv.timer;
2144 bool res = false;
2145 int hgt = guiThreadListHeight/gxTextHeightUtf-1;
2146 if (hgt < 1) hgt = 1;
2147 auto ctm = Timer(true);
2148 foreach (; 0..hgt) {
2149 if (!threadListScrollDown(movecurrent:true)) break;
2150 res = true;
2152 ctm.stop;
2153 if (ChiroTimerExEnabled) writeln("threadListPageDown time: ", ctm);
2154 if (res) setupUnreadTimer();
2155 return res;
2159 // //////////////////////////////////////////////////////////////////// //
2160 static struct WebLink {
2161 int ly; // in lines
2162 int x, len; // in pixels
2163 string url;
2164 string text; // visual text
2165 int attachnum = -1;
2166 bool nofirst = false;
2168 @property bool isAttach () const pure nothrow @safe @nogc { return (attachnum >= 0); }
2171 WebLink[] emurls;
2172 int lastUrlIndex = -1;
2174 void clearDecodedText () {
2175 emurls[] = WebLink.init;
2176 emlines[] = null;
2177 emurls.length = 0;
2178 emurls.assumeSafeAppend;
2179 emlines.length = 0;
2180 emlines.assumeSafeAppend;
2181 lastDecodedUId = 0;
2182 articleTextTopLine = 0;
2183 articleDestTextTopLine = 0;
2184 lastUrlIndex = -1;
2185 arttoname.clear();
2186 arttomail.clear();
2187 artfromname.clear();
2188 artfrommail.clear();
2189 artsubj.clear();
2190 arttime.clear();
2193 // <0: not on url
2194 int findUrlIndexAt (int mx, int my) {
2195 int tpX0 = guiGroupListWidth+2+1;
2196 int tpX1 = screenWidth-1-guiMessageTextLPad*2-guiScrollbarWidth;
2197 int tpY0 = guiThreadListHeight+1;
2198 int tpY1 = screenHeight-1;
2200 int y = tpY0+linesInHeader*gxTextHeightUtf+2+1+guiMessageTextVPad;
2202 if (mx < tpX0 || mx > tpX1) return -1;
2203 if (my < y || my > tpY1) return -1;
2205 mx -= tpX0;
2207 // yeah, i can easily calculate this, i know
2208 uint idx = articleTextTopLine;
2209 while (idx < emlines.length && y < screenHeight) {
2210 if (my >= y && my < y+gxTextHeightUtf) {
2211 foreach (immutable uidx, const ref WebLink wl; emurls) {
2212 //conwriteln("checking url [#", uidx, "]; idx=", idx, "; ly=", wl.ly);
2213 if (wl.ly == idx) {
2214 if (mx >= wl.x && mx < wl.x+wl.len) return cast(int)uidx;
2218 ++idx;
2219 y += gxTextHeightUtf+guiMessageTextInterline;
2222 return -1;
2225 WebLink* findUrlAt (int mx, int my) {
2226 auto uidx = findUrlIndexAt(mx, my);
2227 return (uidx >= 0 ? &emurls[uidx] : null);
2230 private void emlDetectUrls (uint textlines) {
2231 static immutable string[3] protos = [ "https", "http", "ftp" ];
2233 lastUrlIndex = -1;
2235 static sptrdiff urlepos (const(char)[] s, sptrdiff spos) {
2236 assert(spos < s.length);
2237 spos = s.indexOf("://", spos);
2238 assert(spos >= 0);
2239 spos += 3;
2240 // host
2241 while (spos < s.length) {
2242 char ch = s[spos];
2243 if (ch == '/') break;
2244 if (ch <= ' ') return spos;
2245 if (ch != '.' && ch != '-' && !ch.isalnum) return spos;
2246 ++spos;
2248 if (spos >= s.length) return cast(sptrdiff)s.length;
2249 // path
2250 assert(s[spos] == '/');
2251 char[16] brcmap;
2252 usize brlevel = 0;
2253 bool wasSharp = false;
2254 for (; spos < s.length; ++spos) {
2255 import iv.strex : isalnum;
2256 char ch = s[spos];
2257 if (ch <= ' ' || ch == '<' || ch == '>' || ch == '"' || ch == '\'' || ch >= 127) return spos;
2258 // hash
2259 if (ch == '#') {
2260 if (wasSharp) return spos;
2261 wasSharp = true;
2262 brlevel = 0;
2263 continue;
2265 // path delimiter
2266 if (ch == '/') {
2267 brlevel = 0;
2268 continue;
2270 // opening bracket
2271 if (ch == '(' || ch == '{' || ch == '[') {
2272 if (brlevel >= brcmap.length) return spos; // too nested
2273 if (s.length-spos < 2) return spos; // no more chars, ignore
2274 if (!isalnum(s[spos+1]) && s[spos+1] != '_' && s[spos+1] != '%') return spos; // ignore
2275 // looks like URL part
2276 final switch (ch) {
2277 case '(': ch = ')'; break;
2278 case '[': ch = ']'; break;
2279 case '{': ch = '}'; break;
2281 brcmap[brlevel++] = ch;
2282 continue;
2284 // closing bracket
2285 if (ch == ')' || ch == '}' || ch == ']') {
2286 if (brlevel == 0 || ch != brcmap[brlevel-1]) return spos;
2287 --brlevel;
2288 continue;
2290 // other punctuation
2291 if (brlevel == 0 && !isalnum(ch) && ch != '_' && ch != '%') {
2292 // other special chars
2293 if (s.length-spos < 2) return spos; // no more chars, ignore
2294 if (!isalnum(s[spos+1]) && s[spos+1] != '_' && s[spos+1] != '%') {
2295 if (ch == '.' || ch == '!' || ch == ';' || ch == ',' || s[spos+1] != ch) return spos; // ignore
2298 ++spos;
2300 if (spos >= s.length) spos = cast(sptrdiff)s.length;
2301 return spos;
2304 if (textlines > emlines.length) textlines = cast(uint)emlines.length; // just in case
2305 foreach (immutable cy, string s; emlines[0..textlines]) {
2306 if (s.length == 0) continue;
2307 auto pos = s.indexOf("://");
2308 while (pos > 0) {
2309 bool found = false;
2310 auto spos = pos;
2311 foreach (string proto; protos) {
2312 if (spos >= proto.length && proto.strEquCI(s[spos-proto.length..spos])) {
2313 if (spos == proto.length || !s[spos-proto.length-1].isalnum) {
2314 found = true;
2315 spos -= proto.length;
2316 break;
2320 if (found) {
2321 // find URL end
2322 auto epos = urlepos(s, spos);
2323 WebLink wl;
2324 wl.nofirst = (spos > 0);
2325 wl.ly = cast(int)cy;
2326 auto kr = GxKerning(4);
2327 s[0..epos].utfByDCharSPos(delegate (dchar ch, usize stpos) {
2328 int w = kr.fixWidthPre(ch);
2329 if (stpos == spos) wl.x = w;
2331 wl.len = kr.finalWidth-wl.x;
2332 wl.url = wl.text = s[spos..epos];
2333 emurls ~= wl;
2334 pos = epos;
2335 } else {
2336 ++pos;
2338 pos = s.indexOf("://", pos);
2342 int attachcount = 0;
2343 foreach (immutable uint cy; textlines..cast(uint)emlines.length) {
2344 string s = emlines[cy];
2345 if (s.length == 0) continue;
2346 auto spos = s.indexOf("attach:");
2347 if (spos < 0) continue;
2348 auto epos = spos+7;
2349 while (epos < s.length && s.ptr[epos] > ' ') ++epos;
2350 //if (attachcount >= parts.length) break;
2351 WebLink wl;
2352 wl.nofirst = (spos > 0);
2353 wl.ly = cast(int)cy;
2354 //wl.x = gxTextWidthUtf(s[0..spos]);
2355 //wl.len = gxTextWidthUtf(s[spos..epos]);
2356 auto kr = GxKerning(4);
2357 s[0..epos].utfByDCharSPos(delegate (dchar ch, usize stpos) {
2358 int w = kr.fixWidthPre(ch);
2359 if (stpos == spos) wl.x = w;
2361 wl.len = kr.finalWidth-wl.x;
2362 wl.url = wl.text = s[spos..epos];
2363 //if (spos > 0) ++wl.x; // this makes text bolder, no need to
2364 wl.attachnum = attachcount;
2365 //wl.attachfname = s[spos+7..epos];
2366 //wl.part = parts[attachcount];
2367 ++attachcount;
2368 emurls ~= wl;
2372 bool needToDecodeArticleTextNL (uint uid) const nothrow @trusted @nogc {
2373 return (lastDecodedUId != uid);
2376 // fld is locked here
2377 void decodeArticleTextNL (uint uid) {
2378 static auto stmtGet = LazyStatement!"View"(`
2379 SELECT
2380 from_name AS fromName
2381 , from_mail AS fromMail
2382 , to_name AS toName
2383 , to_mail AS toMail
2384 , subj AS subj
2385 , datetime(threads.time, 'unixepoch') AS time
2386 , ChiroUnpack(content_text.content) AS text
2387 , ChiroUnpack(content_html.content) AS html
2388 FROM info
2389 INNER JOIN threads USING(uid)
2390 INNER JOIN content_text USING(uid)
2391 INNER JOIN content_html USING(uid)
2392 WHERE uid=:uid
2393 LIMIT 1
2394 ;`);
2396 if (!needToDecodeArticleTextNL(uid)) return;
2398 //if (uid == 0) { clearDecodedText(); return; }
2399 clearDecodedText();
2400 lastDecodedUId = uid;
2402 // get article content
2403 //FIXME: see `art.getTextContent()` for GPG decryption!
2404 DynStr artcontent;
2405 bool ishtml = false;
2406 bool htmlheader = false;
2407 foreach (auto row; stmtGet.st.bind(":uid", uid).range) {
2408 auto text = row.text!SQ3Text;
2409 auto html = row.html!SQ3Text;
2410 bool isHtml = ((text.xstrip.length == 0 || preferHtmlContent) && html.xstrip.length != 0);
2411 bool forceHtml = false;
2412 if (detectHtmlContent && !isHtml && html.xstrip.length == 0 && text.xstrip.length != 0) {
2413 auto tmp = text.xstrip;
2414 if (tmp.startsWithCI("<!DOCTYPE")) forceHtml = true;
2416 //conwriteln("! ", text.xstrip.length, " ! ", html.xstrip.length);
2417 if (isHtml || forceHtml) {
2418 version(article_can_into_html) {
2419 try {
2420 string s = htmlToText((forceHtml ? text.idup : html.idup), false);
2421 artcontent ~= s;
2422 artcontent.removeASCIICtrls();
2423 htmlheader = true;
2424 } catch (Exception e) {
2425 artcontent ~= (forceHtml ? text : html);
2427 } else {
2428 artcontent ~= (forceHtml ? text : html);
2430 ishtml = true;
2431 } else if (text.length != 0) {
2432 artcontent ~= text;
2433 } else {
2434 artcontent ~= "no text content";
2436 arttoname = row.toName!SQ3Text;
2437 arttomail = row.toMail!SQ3Text;
2438 artfromname = row.fromName!SQ3Text;
2439 artfrommail = row.fromMail!SQ3Text;
2440 artsubj = row.subj!SQ3Text;
2441 arttime = row.time!SQ3Text;
2442 if (artsubj.length == 0) artsubj = "no subject";
2445 if (artcontent.length == 0) return; // no text
2446 emlines ~= null; // hack; this dummy line will be removed
2447 bool skipEmptyLines = true;
2449 if (htmlheader) {
2450 emlines ~= "\x01==============================================";
2451 emlines ~= "\x01--- HTML CONTENT ---";
2452 emlines ~= "\x01==============================================";
2453 //emlines ~= null;
2456 bool lastEndsWithSpace () { return (emlines[$-1].length ? emlines[$-1][$-1] == ' ' : false); }
2458 int lastQLevel = 0;
2460 static string addQuotes(T:const(char)[]) (T s, int qlevel) {
2461 enum QuoteStr = ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ";
2462 if (qlevel <= 0) {
2463 static if (is(T == string) || is (T == typeof(null))) return s; else return s.idup;
2465 if (qlevel > QuoteStr.length-1) qlevel = cast(int)QuoteStr.length-1;
2466 return QuoteStr[$-qlevel-1..$]~s;
2469 // returns quote level
2470 static int removeQuoting (ref ConString s) {
2471 // calculate quote level
2472 int qlevel = 0;
2473 if (s.length && s[0] == '>') {
2474 usize lastqpos = 0, pos = 0;
2475 while (pos < s.length) {
2476 if (s.ptr[pos] != '>') {
2477 if (s.ptr[pos] != ' ') break;
2478 } else {
2479 lastqpos = pos;
2480 ++qlevel;
2482 ++pos;
2484 if (s.length-lastqpos > 1 && s.ptr[lastqpos+1] == ' ') ++lastqpos;
2485 ++lastqpos;
2486 s = s[lastqpos..$];
2488 return qlevel;
2491 bool inCode = false;
2493 void putLine (ConString s) {
2494 int qlevel = (ishtml ? 0 : removeQuoting(s));
2495 //conwriteln("qlevel=", qlevel, "; s=[", s, "]");
2496 // empty line: just insert it
2497 if (s.length == 0) {
2498 if (skipEmptyLines) return;
2499 if (qlevel == 0 && emlines[$-1].xstripright.length == 0) return;
2500 emlines ~= addQuotes(null, qlevel).xstripright;
2501 } else {
2502 if (s.xstrip.length == 0) {
2503 if (skipEmptyLines) return;
2504 if (qlevel == 0 && emlines[$-1].xstripright.length == 0) return;
2506 skipEmptyLines = false;
2507 // join code lines if it is possible
2508 if (inCode && qlevel == lastQLevel && lastEndsWithSpace) {
2509 //conwriteln("<", s, ">");
2510 emlines[$-1] ~= addQuotes(s.xstrip, qlevel);
2511 return;
2513 // two spaces at the beginning usually means "this is code"; don't wrap it
2514 if (s.length >= 1 && s[0] == '\t') {
2515 emlines ~= addQuotes(s, qlevel);
2516 // join next lines if it is possible
2517 inCode = true;
2518 //conwriteln("[", s, "]");
2519 lastQLevel = qlevel;
2520 return;
2522 inCode = false;
2523 // can we append?
2524 bool newline = false;
2525 if (lastQLevel != qlevel || !lastEndsWithSpace) {
2526 newline = true;
2527 } else {
2528 // append words
2529 //while (s.length > 1 && s.ptr[0] <= ' ') s = s[1..$];
2531 while (s.length) {
2532 usize epos = 0;
2533 if (newline) {
2534 while (epos < s.length && s.ptr[epos] <= ' ') ++epos;
2535 } else {
2536 //assert(s[0] > ' ');
2537 while (epos < s.length && s.ptr[epos] <= ' ') ++epos;
2539 while (epos < s.length && s.ptr[epos] > ' ') ++epos;
2540 auto xlen = epos;
2541 while (epos < s.length && s.ptr[epos] <= ' ') ++epos;
2542 if (!newline && emlines[$-1].length+xlen <= 80) {
2543 // no wrapping, continue last line
2544 emlines[$-1] ~= s[0..epos];
2545 } else {
2546 newline = false;
2547 // wrapping; new line
2548 emlines ~= addQuotes(s[0..epos], qlevel);
2550 s = s[epos..$];
2552 if (newline) emlines ~= addQuotes(null, qlevel);
2554 lastQLevel = qlevel;
2557 try {
2558 //foreach (ConString s; LineIterator!false(art.getTextContent)) putLine(s);
2559 const(char)[] buf = artcontent;
2560 while (buf.length) {
2561 usize epos = skipOneLine(buf, 0);
2562 usize eend = epos;
2563 if (eend >= 2 && buf[eend-2] == '\r' && buf[eend-1] == '\n') eend -= 2;
2564 else if (eend >= 1 && buf[eend-1] == '\n') eend -= 1;
2565 putLine(buf[0..eend]);
2566 buf = buf[epos..$];
2568 // remove first dummy line
2569 if (emlines.length) emlines = emlines[1..$];
2570 // remove trailing empty lines
2571 while (emlines.length && emlines[$-1].xstrip.length == 0) emlines.length -= 1;
2572 } catch (Exception e) {
2573 conwriteln("================================= ERROR: ", e.msg, " =================================");
2574 conwriteln(e.toString);
2578 // attaches
2579 auto lcount = cast(uint)emlines.length;
2580 emlinesBeforeAttaches = lcount;
2582 uint attcount = 0;
2583 static auto statAttaches = LazyStatement!"View"(`
2584 SELECT
2585 idx AS idx
2586 , mime AS mime
2587 , name AS name
2588 FROM attaches
2589 WHERE uid=:uid
2590 ORDER BY idx
2591 ;`);
2592 foreach (auto row; statAttaches.st.bind(":uid", uid).range) {
2593 import core.stdc.stdio : snprintf;
2594 if (attcount == 0) { emlines ~= null; emlines ~= null; }
2595 DynStr att;
2596 char[128] buf = void;
2597 //if (type.length == 0) type = "unknown/unknown";
2598 //string s = " [%02s] attach:%s -- %s".format(attcount, Article.fixAttachmentName(filename), type);
2599 auto mime = row.mime!SQ3Text;
2600 auto name = row.name!SQ3Text/*.decodeSubj*/;
2601 auto len = snprintf(buf.ptr, buf.sizeof, " [%2u] attach:%.*s -- %.*s",
2602 attcount, cast(uint)name.length, name.ptr, cast(uint)mime.length, mime.ptr);
2603 assert(len > 0);
2604 if (len > buf.sizeof) len = cast(int)buf.sizeof;
2605 emlines ~= buf[0..len].idup;
2606 ++attcount;
2608 /*FIXME
2609 art.forEachAttachment(delegate(ConString type, ConString filename) {
2610 if (attcount == 0) { emlines ~= null; emlines ~= null; }
2611 import std.format : format;
2612 if (type.length == 0) type = "unknown/unknown";
2613 string s = " [%02s] attach:%s -- %s".format(attcount, Article.fixAttachmentName(filename), type);
2614 emlines ~= s;
2615 ++attcount;
2616 return false;
2619 emlDetectUrls(lcount);
2622 @property int visibleArticleLines () {
2623 int y = guiThreadListHeight+1+linesInHeader*gxTextHeightUtf+2+guiMessageTextVPad;
2624 return (screenHeight-y)/(gxTextHeightUtf+guiMessageTextInterline);
2627 void normalizeArticleTopLine () {
2628 int lines = visibleArticleLines;
2629 if (lines < 1 || emlines.length <= lines) {
2630 articleTextTopLine = 0;
2631 articleDestTextTopLine = 0;
2632 } else {
2633 if (articleTextTopLine < 0) articleTextTopLine = 0;
2634 if (articleTextTopLine+lines > emlines.length) {
2635 articleTextTopLine = cast(int)emlines.length-lines;
2636 if (articleTextTopLine < 0) articleTextTopLine = 0;
2641 void doScrollStep () {
2642 auto oldtop = articleTextTopLine;
2643 foreach (immutable _; 0..6) {
2644 normalizeArticleTopLine();
2645 if (articleDestTextTopLine < articleTextTopLine) {
2646 --articleTextTopLine;
2647 } else if (articleDestTextTopLine > articleTextTopLine) {
2648 ++articleTextTopLine;
2649 } else {
2650 break;
2652 normalizeArticleTopLine();
2654 if (articleTextTopLine == oldtop) {
2655 // can't scroll anymore
2656 articleDestTextTopLine = articleTextTopLine;
2657 return;
2659 postScreenRebuild();
2660 postArticleScroll();
2663 void scrollBy (int delta) {
2664 articleDestTextTopLine += delta;
2665 doScrollStep();
2668 void scrollByPageUp () {
2669 int lines = visibleArticleLines-scrollKeepLines;
2670 if (lines < 1) lines = 1;
2671 scrollBy(-lines);
2674 void scrollByPageDown () {
2675 int lines = visibleArticleLines-scrollKeepLines;
2676 if (lines < 1) lines = 1;
2677 scrollBy(lines);
2680 // ////////////////////////////////////////////////////////////////// //
2681 // this also fixes current/top uids
2682 void getAndFixThreadListIndicies (out int topidx, out int curridx) {
2683 int hgt = guiThreadListHeight/gxTextHeightUtf-1;
2684 if (hgt < 1) hgt = 1;
2686 topidx = chiroGetTreePaneTableUid2Index(msglistTopUId);
2687 immutable origTopIdx = topidx;
2688 if (topidx < 0) topidx = 0;
2689 curridx = chiroGetTreePaneTableUid2Index(msglistCurrUId);
2690 immutable origCurrIdx = curridx;
2691 if (curridx < 0) curridx = 0;
2693 //conwriteln("topuid: ", msglistTopUId, "; topidx=", topidx);
2694 //conwriteln("curruid: ", msglistCurrUId, "; curridx=", curridx);
2696 if (curridx-3 < topidx) {
2697 topidx = curridx-3;
2698 if (topidx < 0) topidx = 0;
2700 if (curridx+3 > topidx+hgt) {
2701 topidx = (curridx+3 > hgt ? curridx+3-hgt : 0);
2702 if (topidx < 0) topidx = 0;
2705 if (origCurrIdx != curridx || msglistCurrUId == 0) {
2706 immutable ocurr = msglistCurrUId;
2707 msglistCurrUId = chiroGetTreePaneTableIndex2Uid(curridx);
2708 if (ocurr != msglistCurrUId) {
2709 threadListPositionDirty();
2710 setupUnreadTimer();
2714 if (origTopIdx != topidx || msglistTopUId == 0) {
2715 immutable otop = msglistTopUId;
2716 msglistTopUId = chiroGetTreePaneTableIndex2Uid(topidx);
2717 if (otop != msglistTopUId) threadListPositionDirty();
2721 void setupUnreadTimer () {
2722 postMarkAsUnreadEvent();
2725 // //////////////////////////////////////////////////////////////////// //
2726 //TODO: move parts to widgets
2727 override void onPaint () {
2728 viewDataVersion = dbView.getDataVersion();
2729 gxClipReset();
2731 gxFillRect(0, 0, guiGroupListWidth, screenHeight, getColor("grouplist-back"));
2732 gxVLine(guiGroupListWidth, 0, screenHeight, getColor("grouplist-divline"));
2734 gxFillRect(guiGroupListWidth+1, 0, screenWidth, guiThreadListHeight, getColor("threadlist-back"));
2735 gxHLine(guiGroupListWidth+1, guiThreadListHeight, screenWidth, getColor("threadlist-divline"));
2737 // ////////////////////////////////////////////////////////////////// //
2738 void drawArticle (uint uid) {
2739 import core.stdc.stdio : snprintf;
2740 import std.format : format;
2741 import std.datetime;
2742 char[128] tbuf;
2743 const(char)[] tbufs;
2745 void xfmt (string s, const(char)[][] strs...) {
2746 int dpos = 0;
2747 void puts (const(char)[] s...) {
2748 foreach (char ch; s) {
2749 if (dpos >= tbuf.length) break;
2750 tbuf[dpos++] = ch;
2753 while (s.length) {
2754 if (strs.length && s.length > 1 && s[0] == '%' && s[1] == 's') {
2755 puts(strs[0]);
2756 strs = strs[1..$];
2757 s = s[2..$];
2758 } else {
2759 puts(s[0]);
2760 s = s[1..$];
2763 tbufs = tbuf[0..dpos];
2766 if (needToDecodeArticleTextNL(uid)) {
2767 decodeArticleTextNL(uid);
2771 gxClipX0 = guiGroupListWidth+2;
2772 gxClipX1 = screenWidth-1;
2773 gxClipY0 = guiThreadListHeight+1;
2774 gxClipY1 = screenHeight-1;
2776 gxClipRect = GxRect(GxPoint(guiGroupListWidth+2, guiThreadListHeight+1), GxPoint(screenWidth-1, screenHeight-1));
2778 int msx = lastMouseX;
2779 int msy = lastMouseY;
2781 int curDrawYMul = 1;
2783 // header
2784 immutable int hdrHeight = (3+(arttoname.length || arttomail.length ? 1 : 0))*gxTextHeightUtf+2;
2785 gxFillRect(gxClipRect.x0, gxClipRect.y0, gxClipRect.x1-gxClipRect.x0+1, hdrHeight, getColor("msg-header-back"));
2787 if (artfromname.length) {
2788 xfmt("From: %s <%s>", artfromname, artfrommail);
2789 } else {
2790 xfmt("From: %s", artfrommail);
2792 gxDrawTextUtf(gxClipRect.x0+guiMessageTextLPad, gxClipRect.y0+0*gxTextHeightUtf+1, tbufs, getColor("msg-header-from"));
2793 if (arttoname.length || arttomail.length) {
2794 if (arttoname.length) {
2795 xfmt("To: %s <%s>", arttoname, arttomail);
2796 } else {
2797 xfmt("To: %s", arttomail);
2799 gxDrawTextUtf(gxClipRect.x0+guiMessageTextLPad, gxClipRect.y0+curDrawYMul*gxTextHeightUtf+1, tbufs, getColor("msg-header-to"));
2800 ++curDrawYMul;
2802 xfmt("Subject: %s", (artsubj.length ? artsubj : "no subject"));
2803 gxDrawTextUtf(gxClipRect.x0+guiMessageTextLPad, gxClipRect.y0+curDrawYMul*gxTextHeightUtf+1, tbufs, getColor("msg-header-subj"));
2804 ++curDrawYMul;
2806 //auto t = SysTime.fromUnixTime(arttime);
2807 //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);
2808 auto tlen = snprintf(tbuf.ptr, tbuf.length, "Date: %.*s", cast(uint)arttime.length, arttime.ptr);
2809 gxDrawTextUtf(gxClipRect.x0+guiMessageTextLPad, gxClipRect.y0+curDrawYMul*gxTextHeightUtf+1, tbuf[0..tlen], getColor("msg-header-date"));
2810 ++curDrawYMul;
2814 // text
2815 linesInHeader = curDrawYMul;
2816 int y = gxClipRect.y0+curDrawYMul*gxTextHeightUtf+2;
2818 gxHLine(gxClipRect.x0, y, gxClipRect.x1-gxClipRect.x0+1, getColor("msg-header-divline"));
2819 ++y;
2820 gxFillRect(gxClipRect.x0, y, gxClipRect.x1-gxClipRect.x0+1, screenHeight-y, getColor("msg-text-back"));
2821 y += guiMessageTextVPad;
2823 immutable sty = y;
2825 normalizeArticleTopLine();
2827 bool drawUpMark = (articleTextTopLine > 0);
2828 bool drawDownMark = false;
2830 immutable uint messageTextNormalColor = getColor("msg-text-text");
2831 immutable uint messageTextQuote0Color = getColor("msg-text-quote0");
2832 immutable uint messageTextQuote1Color = getColor("msg-text-quote1");
2833 immutable uint messageTextHtmlHeaderColor = getColor("msg-text-html-sign");
2834 immutable uint messageTextLinkColor = getColor("msg-text-link");
2835 immutable uint messageTextLinkHoverColor = getColor("msg-text-link", "hover");
2836 immutable uint messageTextLinkPressedColor = getColor("msg-text-link", "pressed");
2838 uint idx = articleTextTopLine;
2839 bool msvisible = isMouseVisible;
2840 bool checkQuotes = true;
2841 if (emlines.length && emlines[0].length && emlines[0][0] == 1) {
2842 // html content
2843 checkQuotes = false;
2845 while (idx < emlines.length && y < screenHeight) {
2846 int qlevel = 0;
2847 string s = emlines[idx];
2849 if (checkQuotes) {
2850 foreach (immutable char ch; s) {
2851 if (ch <= ' ') continue;
2852 if (ch != '>') break;
2853 ++qlevel;
2857 uint clr = messageTextNormalColor;
2858 if (qlevel) {
2859 final switch (qlevel%2) {
2860 case 0: clr = messageTextQuote0Color; break;
2861 case 1: clr = messageTextQuote1Color; break;
2865 if (!checkQuotes && s.length && s[0] == 1) {
2866 clr = messageTextHtmlHeaderColor;
2867 s = s[1..$];
2870 gxDrawTextUtf(GxDrawTextOptions.TabColor(4, clr), gxClipRect.x0+guiMessageTextLPad, y, s);
2872 foreach (const ref WebLink wl; emurls) {
2873 if (wl.ly == idx) {
2874 uint lclr = messageTextLinkColor;
2875 if (msvisible && msy >= y && msy < y+gxTextHeightUtf &&
2876 msx >= gxClipRect.x0+1+guiMessageTextLPad+wl.x &&
2877 msx < gxClipRect.x0+1+guiMessageTextLPad+wl.x+wl.len)
2879 lclr = (lastMouseLeft ? messageTextLinkPressedColor : messageTextLinkHoverColor);
2880 //lclr = getColor("msg-text-link", (lastMouseLeft ? "pressed" : "hover"));
2882 gxDrawTextUtf(GxDrawTextOptions.TabColorFirstFull(4, lclr, wl.nofirst), gxClipRect.x0+guiMessageTextLPad+wl.x, y, wl.text);
2886 if (gxClipRect.y1-y < gxTextHeightUtf && emlines.length-idx > 0) drawDownMark = true;
2888 ++idx;
2889 y += gxTextHeightUtf+guiMessageTextInterline;
2892 //gxDrawTextOutScaledUtf(1, gxClipRect.x1-gxTextWidthUtf(triangleDownStr)-3, sty, triangleDownStr, (drawUpMark ? gxRGB!(255, 255, 255) : gxRGB!(85, 85, 85)), gxRGB!(0, 69, 69));
2893 //gxDrawTextOutScaledUtf(1, gxClipRect.x1-gxTextWidthUtf(triangleUpStr)-3, gxClipRect.y1-7, triangleUpStr, (drawDownMark ? gxRGB!(255, 255, 255) : gxRGB!(85, 85, 85)), gxRGB!(0, 69, 69));
2895 gxDrawScrollBar(GxRect(gxClipRect.x1-10, sty+10, 5, gxClipRect.y1-sty-17), cast(int)emlines.length-1, idx-1);
2897 bool twited = false;
2898 DynStr twittext = chiroGetMessageTwit(lastDecodedTid, uid, out twited);
2899 if (twited) {
2900 immutable uint clr = getColor("twit-shade");
2902 foreach (immutable dy; gxClipRect.y0+3*gxTextHeightUtf+2..gxClipRect.y1+1) {
2903 foreach (immutable dx; gxClipRect.x0..gxClipRect.x1+1) {
2904 if ((dx^dy)&1) gxPutPixel(dx, dy, clr);
2908 GxRect rc = gxClipRect;
2909 rc.y0 = rc.y0+3*gxTextHeightUtf+2;
2910 gxDashRect(rc, clr);
2912 if (twittext.length) {
2913 int tx = gxClipRect.x0+(gxClipRect.width-gxTextWidthScaledUtf(2, twittext))/2-1;
2914 int ty = gxClipRect.y0+(gxClipRect.height-3*gxTextHeightUtf)/2-1;
2915 gxDrawTextOutScaledUtf(2, tx, ty, twittext, getColor("twit-text"), getColor("twit-outline"));
2920 // ////////////////////////////////////////////////////////////////// //
2921 void drawThreadList () {
2922 uint tid = 0;
2923 if (folderCurrIndex >= 0 && folderCurrIndex < folderList.length) tid = folderList[folderCurrIndex].tagid;
2924 switchToFolderTid(tid);
2926 // find indicies
2927 int topidx, curridx;
2928 getAndFixThreadListIndicies(out topidx, out curridx);
2930 int hgt = (guiThreadListHeight+gxTextHeightUtf-1)/gxTextHeightUtf;
2931 if (hgt < 1) hgt = 1;
2933 gxClipRect.x0 = guiGroupListWidth+2;
2934 gxClipRect.x1 = screenWidth-1-5;
2935 gxClipRect.y0 = 0;
2936 gxClipRect.y1 = guiThreadListHeight-1;
2937 immutable uint origX0 = gxClipRect.x0;
2938 immutable uint origX1 = gxClipRect.x1;
2939 immutable uint origY0 = gxClipRect.y0;
2940 immutable uint origY1 = gxClipRect.y1;
2941 int y = 0;
2943 static uint darkenBy (in uint clr, in int val) pure nothrow @safe @nogc {
2944 pragma(inline, true);
2945 return
2946 val ?
2947 (clr&0xff_00_00_00)|
2948 (cast(uint)clampToByte(cast(int)(cast(ubyte)(clr>>16))-val)<<16)|
2949 (cast(uint)clampToByte(cast(int)(cast(ubyte)(clr>>8))-val)<<8)|
2950 (cast(uint)clampToByte(cast(int)(cast(ubyte)clr)-val)) :
2951 clr;
2954 chiroGetPaneTablePage(topidx, hgt,
2955 delegate (int pgofs, /* offset from the page start, from zero and up to `limit` */
2956 int iid, /* item id, never zero */
2957 uint uid, /* msguid, never zero */
2958 uint parentuid, /* parent msguid, may be zero */
2959 uint level, /* threading level, from zero */
2960 Appearance appearance, /* see above */
2961 Mute mute, /* see above */
2962 SQ3Text date, /* string representation of receiving date and time */
2963 SQ3Text subj, /* message subject, can be empty string */
2964 SQ3Text fromName, /* message sender name, can be empty string */
2965 SQ3Text fromMail, /* message sender email, can be empty string */
2966 SQ3Text title) /* title from twiting */
2968 import std.format : format;
2969 import std.datetime;
2971 if (y >= guiThreadListHeight) return;
2972 if (subj.length == 0) subj = "no subject";
2974 gxClipRect.x0 = origX0;
2975 gxClipRect.x1 = origX1;
2977 int darken = (level != 0 && appearance != Appearance.Unread ? 40 : 0);
2979 string style = "normal";
2980 if (appearance == Appearance.Unread) {
2981 style = "unread";
2982 darken = 0;
2983 } else {
2984 if (mute > Mute.Normal) { style = "twit"; darken = 0; }
2986 if (isSoftDeleted(appearance)) {
2987 darken = 0;
2988 style = (appearance == Appearance.SoftDeletePurge ? "hard-del" : "soft-del");
2991 char[64] stfull = void;
2992 usize stpos = 0;
2993 if (uid == msglistCurrUId) {
2994 static immutable string sc = "cursor-";
2995 stpos = sc.length;
2996 stfull[0..stpos] = sc;
2998 stfull[stpos..stpos+style.length] = style;
2999 const(char)[] stname = stfull[0..stpos+style.length];
3002 //conwriteln("STNAME: <", stname, ">; darken=", darken);
3004 immutable uint clrBack = getColor("threadlist-back", stname);
3005 immutable uint clrOut = getColor("threadlist-outline", stname);
3006 immutable uint clrFrom = darkenBy(getColor("threadlist-from-text", stname), darken);
3007 immutable uint clrMail = darkenBy(getColor("threadlist-mail-text", stname), darken);
3008 immutable uint clrSubj = darkenBy(getColor("threadlist-subj-text", stname), darken);
3009 immutable uint clrTime = darkenBy(getColor("threadlist-time-text", stname), darken);
3011 // background
3012 if (!gxIsTransparent(clrBack)) gxFillRect(gxClipRect.x0, y, gxClipRect.width-1, gxTextHeightUtf, clrBack);
3013 gxClipRect.x0 = gxClipRect.x0+1;
3014 gxClipRect.x1 = gxClipRect.x1-1;
3016 // time
3017 int timewdt = gxDrawTextOutScaledUtf(1, gxClipRect.x1-gxTextWidthUtf(date), y, date, clrTime, clrOut);
3018 if (timewdt%8) timewdt = (timewdt|7)+1;
3020 SQ3Text from = fromName;
3022 auto vp = from.indexOf(" via Digitalmars-");
3023 if (vp > 0) {
3024 from = from[0..vp].xstrip;
3025 if (from.length == 0) from = "anonymous";
3029 // from/mail
3030 gxClipRect.x1 = gxClipRect.x1-/*(13*6+4)*2+33*/timewdt;
3031 enum FromWidth = 22*6*2+88;
3032 gxDrawTextOutScaledUtf(1, gxClipRect.x1-FromWidth, y, from, clrFrom, clrOut);
3033 gxDrawTextOutScaledUtf(1, gxClipRect.x1-FromWidth+gxTextWidthUtf(from)+4, y, "<", clrMail, clrOut);
3034 gxDrawTextOutScaledUtf(1, gxClipRect.x1-FromWidth+gxTextWidthUtf(from)+4+gxTextWidthUtf("<")+1, y, fromMail, clrMail, clrOut);
3035 gxDrawTextOutScaledUtf(1, gxClipRect.x1-FromWidth+gxTextWidthUtf(from)+4+gxTextWidthUtf("<")+1+gxTextWidthUtf(fromMail)+1, y, ">", clrMail, clrOut);
3036 gxClipRect.x1 = gxClipRect.x1-FromWidth-6;
3038 // subj
3039 gxDrawTextOutScaledUtf(1, gxClipRect.x0+level*3, y, subj, clrSubj, clrOut);
3041 // nesting dots
3042 if (level) {
3043 immutable uint clrDot = getColor("threadlist-dots", stname);
3044 foreach (immutable dx; 0..level) gxPutPixel(gxClipRect.x0+1+dx*3, y+gxTextHeightUtf/2, clrDot);
3047 // deleted strike line
3048 if (isSoftDeleted(appearance)) {
3049 immutable uint clrLine = getColor("threadlist-strike-line", stname);
3050 if (!gxIsTransparent(clrLine)) {
3051 gxClipRect.x0 = origX0;
3052 gxClipRect.x1 = origX1;
3053 gxHLine(gxClipRect.x0, y+gxTextHeightUtf/2, gxClipRect.x1-gxClipRect.x0+1, clrLine);
3054 if (appearance == Appearance.SoftDeletePurge) {
3055 gxHLine(gxClipRect.x0, y+gxTextHeightUtf/2+1, gxClipRect.x1-gxClipRect.x0+1, clrLine);
3060 y += gxTextHeightUtf;
3064 // draw progressbar
3065 if (msglistCurrUId) {
3066 gxClipRect.x0 = origX0;
3067 gxClipRect.x1 = origX1+5;
3068 gxClipRect.y0 = origY0;
3069 gxClipRect.y1 = origY1;
3070 gxDrawScrollBar(GxRect(gxClipRect.x1-5, gxClipRect.y0, 4, gxClipRect.height-1),
3071 cast(int)chiroGetTreePaneTableCount()-1, curridx);
3074 drawArticle(msglistCurrUId);
3077 // ////////////////////////////////////////////////////////////////// //
3078 void drawFolders () {
3079 immutable uint clrNormal = getColor("grouplist-normal-text");
3080 immutable uint clrNormalCursor = getColor("grouplist-normal-text", "cursor");
3081 immutable uint clrNormalChild = getColor("grouplist-normal-child-text");
3082 immutable uint clrNormalChildCursor = getColor("grouplist-normal-child-text", "cursor");
3083 immutable uint clrDots = getColor("grouplist-dots");
3084 immutable uint clrNormOutline = getColor("grouplist-outline");
3085 immutable uint clrCurOutline = getColor("grouplist-outline", "cursor");
3087 folderMakeCurVisible();
3088 int ofsx = 2;
3089 int ofsy = 1;
3090 foreach (immutable idx, const FolderInfo fi; folderList) {
3091 if (idx < folderTopIndex) continue;
3092 if (ofsy >= screenHeight) break;
3093 gxClipReset();
3095 immutable int depth = fi.depth;
3096 uint clr = (depth ? clrNormalChild : clrNormal);
3097 uint clrOut = clrNormOutline;
3099 string curstyle = null;
3100 if (idx == folderCurrIndex) {
3101 curstyle = "cursor";
3102 gxFillRect(0, ofsy-1, guiGroupListWidth, (gxTextHeightUtf+2), getColor("grouplist-back", curstyle));
3103 clr = (depth ? clrNormalChildCursor : clrNormalCursor);
3104 clrOut = clrCurOutline;
3106 gxClipRect.x0 = ofsx-1;
3107 gxClipRect.y0 = ofsy;
3108 gxClipRect.x1 = guiGroupListWidth-3;
3110 if (fi.unreadCount) {
3111 clr = getColor("grouplist-unread-text", curstyle);
3112 } else if (depth == 0) {
3113 if (fi.name == "#spam") clr = getColor("grouplist-spam-text", curstyle);
3114 else if (fi.name == "/accounts") clr = getColor("grouplist-accounts-text", curstyle);
3115 } else if (depth == 1 && fi.name.startsWith("/accounts/")) {
3116 clr = getColor("grouplist-accounts-child-text", curstyle);
3117 } else if (fi.name.startsWith("/accounts/") && fi.name.endsWith("/inbox")) {
3118 clr = getColor("grouplist-inbox-text", curstyle);
3120 foreach (immutable dd; 0..depth) gxPutPixel(ofsx+dd*6+2, ofsy+gxTextHeightUtf/2, clrDots);
3121 gxDrawTextOutScaledUtf(1, ofsx+depth*6, ofsy, fi.visname, clr, clrOut);
3122 ofsy += gxTextHeightUtf+2;
3126 drawFolders();
3127 drawThreadList();
3128 //setupTrayAnimation();
3130 version(test_round_rect) {
3131 gxClipReset();
3132 gxFillRoundedRect(lastMouseX-16, lastMouseY-16, 128, 96, rrad, /*gxSolidWhite*/gxRGBA!(0, 255, 0, 127));
3133 //gxDrawRoundedRect(lastMouseX-16, lastMouseY-16, 128, 96, rrad, gxRGBA!(0, 255, 0, 127));
3137 version(test_round_rect) {
3138 int rrad = 16;
3141 override bool onKeySink (KeyEvent event) {
3142 if (event.pressed) {
3143 version(test_round_rect) {
3144 if (event == "Plus") { ++rrad; postScreenRebuild(); return true; }
3145 if (event == "Minus") { --rrad; postScreenRebuild(); return true; }
3147 if (dbg_dump_keynames) conwriteln("key: ", event.toStr, ": ", event.modifierState&ModifierState.windows);
3148 //foreach (const ref kv; mainAppKeyBindings.byKeyValue) if (event == kv.key) concmd(kv.value);
3149 char[64] kname;
3150 if (auto cmdp = event.toStrBuf(kname[]) in mainAppKeyBindings) {
3151 concmd(*cmdp);
3152 return true;
3154 // debug
3156 if (event == "S-Up") {
3157 if (folderTop > 0) --folderTop;
3158 postScreenRebuild();
3159 return true;
3161 if (event == "S-Down") {
3162 if (folderTop+1 < folders.length) ++folderTop;
3163 postScreenRebuild();
3164 return true;
3167 //if (event == "Tab") { new PostWindow(); return true; }
3168 //if (event == "Tab") { new SelectPopBoxWindow(null); return true; }
3170 return super.onKeySink(event);
3173 // returning `false` to avoid screen rebuilding by dispatcher
3174 override bool onMouseSink (MouseEvent event) {
3175 int mx = event.x;
3176 int my = event.y;
3177 // button press
3178 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
3179 // select folder
3180 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < screenHeight) {
3181 uint fnum = my/(gxTextHeightUtf+2)+folderTopIndex;
3182 if (fnum >= 0 && fnum != folderCurrIndex && folderCurrIndex < folderList.length) {
3183 folderCurrIndex = fnum;
3184 postScreenRebuild();
3186 return false;
3188 // select post
3189 if (mx > guiGroupListWidth && mx < screenWidth && my >= 0 && my < guiThreadListHeight) {
3190 if (lastDecodedTid != 0) {
3191 my /= gxTextHeightUtf;
3192 // find indicies
3193 int topidx, curridx;
3194 getAndFixThreadListIndicies(out topidx, out curridx);
3195 int newidx = topidx+my;
3196 if (curridx != newidx) {
3197 uint newuid = chiroGetTreePaneTableIndex2Uid(newidx);
3198 if (newuid && newuid != msglistCurrUId) {
3199 chiroSetMessageRead(lastDecodedTid, newuid);
3200 msglistCurrUId = newuid;
3201 setupTrayAnimation();
3202 postScreenRebuild();
3205 return false;
3207 } else {
3208 auto uidx = findUrlIndexAt(mx, my);
3209 if (uidx != lastUrlIndex || lastUrlIndex >= 0) { lastUrlIndex = uidx; postScreenRebuild(); }
3212 // wheel
3213 if (event.type == MouseEventType.buttonPressed && (event.button == MouseButton.wheelUp || event.button == MouseButton.wheelDown)) {
3214 // folder
3215 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < screenHeight) {
3216 if (event.button == MouseButton.wheelUp) {
3217 if (folderCurrIndex > 0) {
3218 --folderCurrIndex;
3219 postScreenRebuild();
3221 } else {
3222 if (folderCurrIndex+1 < folderList.length) {
3223 ++folderCurrIndex;
3224 postScreenRebuild();
3227 return false;
3229 // post
3230 if (mx > guiGroupListWidth && mx < screenWidth && my >= 0 && my < guiThreadListHeight) {
3231 if (event.button == MouseButton.wheelUp) {
3232 if (threadListUp()) postScreenRebuild();
3233 } else {
3234 if (threadListDown()) postScreenRebuild();
3236 return false;
3238 // text
3239 if (mx > guiGroupListWidth && mx < screenWidth && my > guiThreadListHeight && my < screenHeight) {
3240 enum ScrollLines = 2;
3241 if (event.button == MouseButton.wheelUp) scrollBy(-ScrollLines); else scrollBy(ScrollLines);
3242 postScreenRebuild();
3243 return false;
3246 // button release
3247 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
3248 // try url
3249 auto uidx = findUrlIndexAt(mx, my);
3250 auto url = findUrlAt(mx, my);
3251 if (url !is null) {
3252 if (url.isAttach) {
3253 if (event.modifierState&(ModifierState.alt|ModifierState.shift|ModifierState.ctrl)) {
3254 concmdf!"attach_save %s"(url.attachnum);
3255 } else {
3256 concmdf!"attach_view %s"(url.attachnum);
3258 } else {
3259 if (event.modifierState&(ModifierState.shift|ModifierState.ctrl)) {
3260 //conwriteln("link-to-clipboard: <", url.url, ">");
3261 setClipboardText(vbwin, url.url); // it is safe to cast here
3262 setPrimarySelection(vbwin, url.url); // it is safe to cast here
3263 conwriteln("url copied to the clipboard.");
3264 } else {
3265 //conwriteln("link-open: <", url.url, ">");
3266 concmdf!"open_url \"%s\" %s"(url.url, ((event.modifierState&ModifierState.alt) != 0));
3269 postScreenRebuild();
3270 } else {
3271 if (lastUrlIndex >= 0) postScreenRebuild();
3273 lastUrlIndex = uidx;
3275 if (event.type == MouseEventType.motion) {
3276 auto uidx = findUrlIndexAt(mx, my);
3277 if (uidx != lastUrlIndex) { lastUrlIndex = uidx; postScreenRebuild(); return false; }
3281 if (event.type == MouseEventType.buttonPressed || event.type == MouseEventType.buttonReleased) {
3282 postScreenRebuild();
3283 } else {
3284 // for OpenGL, this rebuilds the whole screen anyway
3285 postScreenRepaint();
3289 return false;
3294 // ////////////////////////////////////////////////////////////////////////// //
3295 __gshared LockFile mainLockFile;
3298 void checkMainLockFile () {
3299 import std.path : buildPath;
3300 mainLockFile = LockFile(buildPath(mailRootDir, ".chiroptera2.lock"));
3301 if (!mainLockFile.tryLock) {
3302 mainLockFile.close();
3303 //assert(0, "already running");
3304 conwriteln("another copy of Chiroptera is running, disabling updater.");
3305 receiverDisable();
3310 void main (string[] args) {
3312 import etc.linux.memoryerror;
3313 bool setMH = true;
3314 int idx = 1;
3315 while (idx < args.length) {
3316 string a = args[idx++];
3317 if (a == "--") break;
3318 if (a == "--gdb") {
3319 setMH = false;
3320 --idx;
3321 foreach (immutable c; idx+1..args.length) args[c-1] = args[c];
3324 if (setMH) registerMemoryErrorHandler();
3327 defaultColorStyle.parseStyle(ChiroStyle);
3329 glconAllowOpenGLRender = false;
3331 checkMainLockFile();
3332 scope(exit) mainLockFile.close();
3334 sdpyWindowClass = "Chiroptera";
3335 //glconShowKey = "M-Grave";
3337 initConsole();
3338 //FIXME
3339 //hitwitInitConsole();
3341 clearBindings();
3342 setupDefaultBindings();
3344 concmd("exec chiroptera.rc tan");
3345 concmd("load_style userstyle.rc");
3347 //FIXME
3348 //scanFolders();
3350 //FIXME:concmdf!"exec %s/accounts.rc tan"(mailRootDir);
3351 //FIXME:concmdf!"exec %s/addressbook.rc tan"(mailRootDir);
3352 //FIXME:concmdf!"exec %s/filters.rc tan"(mailRootDir);
3353 //FIXME:concmdf!"exec %s/highlights.rc tan"(mailRootDir);
3354 //FIXME:concmdf!"exec %s/twits.rc tan"(mailRootDir);
3355 //FIXME:concmdf!"exec %s/twit_threads.rc tan"(mailRootDir);
3356 //FIXME:concmdf!"exec %s/auto_twits.rc tan"(mailRootDir);
3357 //FIXME:concmdf!"exec %s/auto_twit_threads.rc tan"(mailRootDir);
3358 //FIXME:concmdf!"exec %s/repreps.rc tan"(mailRootDir);
3359 conProcessQueue(); // load config
3360 conProcessArgs!true(args);
3362 chiroOpenStorageDB();
3363 chiroOpenViewDB();
3364 chiroOpenConfDB();
3365 //ChiroTimerEnabled = true;
3366 //ChiroTimerExEnabled = true;
3368 rescanFolders();
3370 egraCreateSystemWindow("Chiroptera", allowResize:true);
3372 static if (is(typeof(&vbwin.closeQuery))) {
3373 vbwin.closeQuery = delegate () { concmd("quit"); egraPostDoConCommands(); };
3377 vbwin.windowResized = delegate (int wdt, int hgt) {
3378 egraSdpyOnWindowResized(wdt, hgt);
3380 // TODO: fix gui sizes
3381 if (vbwin.closed) return;
3383 double glwFrac = cast(double)guiGroupListWidth/screenWidth;
3384 double tlhFrac = cast(double)guiThreadListHeight/screenHeight;
3386 if (wdt < screenEffScale*32) wdt = screenEffScale;
3387 if (hgt < screenEffScale*32) hgt = screenEffScale;
3388 int newwdt = (wdt+screenEffScale-1)/screenEffScale;
3389 int newhgt = (hgt+screenEffScale-1)/screenEffScale;
3391 guiGroupListWidth = cast(int)(glwFrac*newwdt+0.5);
3392 guiThreadListHeight = cast(int)(tlhFrac*newhgt+0.5);
3394 if (guiGroupListWidth < 12) guiGroupListWidth = 12;
3395 if (guiThreadListHeight < 16) guiThreadListHeight = 16;
3399 vbwin.addEventListener((QuitEvent evt) {
3400 if (vbwin.closed) return;
3401 if (isQuitRequested) { vbwin.close(); return; }
3402 vbwin.close();
3406 vbwin.addEventListener((TrayAnimationStepEvent evt) {
3407 if (vbwin.closed) return;
3408 if (isQuitRequested) { vbwin.close(); return; }
3409 trayDoAnimationStep();
3413 HintWindow uphintWindow;
3415 vbwin.addEventListener((UpdatingAccountEvent evt) {
3416 DynStr accName = chiroGetAccountName(evt.accid);
3417 if (accName.length) {
3418 DynStr msg = "updating: ";
3419 msg ~= accName;
3420 if (uphintWindow !is null) {
3421 uphintWindow.message = msg;
3422 } else {
3423 uphintWindow = new HintWindow(msg);
3424 uphintWindow.y0 = guiThreadListHeight+1+(3*gxTextHeightUtf+2-uphintWindow.height)/2;
3426 postScreenRebuild();
3430 vbwin.addEventListener((UpdatingAccountCompleteEvent evt) {
3431 if (uphintWindow is null) return;
3432 DynStr accName = chiroGetAccountName(evt.accid);
3433 if (accName.length) {
3434 DynStr msg = "done: ";
3435 msg ~= accName;
3436 uphintWindow.message = msg;
3437 postScreenRebuild();
3441 vbwin.addEventListener((UpdatingCompleteEvent evt) {
3442 if (uphintWindow) {
3443 uphintWindow.close();
3444 uphintWindow = null;
3446 if (vbwin is null || vbwin.closed) return;
3447 setupTrayAnimation(); // check if we have to start/stop animation, and do it
3448 postScreenRebuild();
3451 vbwin.addEventListener((TagThreadsUpdatedEvent evt) {
3452 if (vbwin is null || vbwin.closed) return;
3453 if (mainPane is null) return;
3454 if (evt.tagid && mainPane.lastDecodedTid == evt.tagid) {
3455 // force view pane rebuild
3456 mainPane.switchToFolderTid(evt.tagid, forced:true);
3457 postScreenRebuild();
3462 ProgressWindow recalcHintWindow;
3464 vbwin.addEventListener((RecalcAllTwitsEvent evt) {
3465 if (vbwin !is null && !vbwin.closed) {
3466 glconHide();
3467 if (recalcHintWindow !is null) recalcHintWindow.close();
3468 recalcHintWindow = new ProgressWindow("recalculating twits");
3469 egraRebuildScreen();
3470 } else {
3471 recalcHintWindow = null;
3474 disableMailboxUpdates();
3475 scope(exit) enableMailboxUpdates();
3476 chiroRecalcAllTwits((msg, curr, total) {
3477 if (recalcHintWindow is null) return;
3478 if (recalcHintWindow.setProgress(msg, curr, total)) {
3479 egraRebuildScreen();
3483 if (vbwin !is null && !vbwin.closed) {
3484 if (recalcHintWindow !is null) recalcHintWindow.close();
3485 if (mainPane !is null) mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
3486 postScreenRebuild();
3490 vbwin.addEventListener((ArticleTextScrollEvent evt) {
3491 if (vbwin is null || vbwin.closed) return;
3492 if (mainPane is null) return;
3493 mainPane.doScrollStep();
3496 vbwin.addEventListener((MarkAsUnreadEvent evt) {
3497 if (vbwin is null || vbwin.closed) return;
3498 if (mainPane is null) return;
3499 //conwriteln("unread timer fired");
3500 if (mainPane.lastDecodedTid == evt.tagid && evt.uid == mainPane.msglistCurrUId) {
3501 //conwriteln("*** unread timer hit!");
3502 chiroSetMessageRead(evt.tagid, evt.uid);
3503 setupTrayAnimation();
3504 postScreenRebuild();
3508 void firstTimeInit () {
3509 // create notification icon
3510 if (trayicon is null) {
3511 auto drv = vfsAddPak(wrapMemoryRO(iconsZipData[]), "", "databinz/icons.zip:");
3512 scope(exit) vfsRemovePak(drv);
3513 try {
3514 foreach (immutable idx; 0..6) {
3515 string fname = "databinz/icons.zip:icons";
3516 if (idx == 0) {
3517 fname ~= "/main.png";
3518 } else {
3519 import std.format : format;
3520 fname = "%s/bat%s.png".format(fname, idx-1);
3522 auto fl = VFile(fname);
3523 if (fl.size == 0 || fl.size > 1024*1024) throw new Exception("fucked icon");
3524 auto pngraw = new ubyte[](cast(uint)fl.size);
3525 fl.rawReadExact(pngraw);
3526 auto img = readPng(pngraw);
3527 if (img is null) throw new Exception("fucked icon");
3528 icons[idx] = imageFromPng(img);
3530 foreach (immutable idx, MemoryImage img; icons[]) {
3531 trayimages[idx] = Image.fromMemoryImage(img);
3533 vbwin.icon = icons[0];
3534 trayicon = new NotificationAreaIcon("Chiroptera", trayimages[0], (MouseButton button) {
3535 scope(exit) if (!conQueueEmpty()) egraPostDoConCommands();
3536 if (button == MouseButton.left) vbwin.switchToWindow();
3537 if (button == MouseButton.middle) concmd("quit");
3539 setupTrayAnimation();
3540 flushGui(); // or it may not redraw itself
3541 } catch (Exception e) {
3542 conwriteln("ERROR loading icons: ", e.msg);
3547 vbwin.visibleForTheFirstTime = delegate () {
3548 egraFirstTimeInit();
3549 firstTimeInit();
3552 mainPane = new MainPaneWindow();
3553 egraSkipScreenClear = true; // main pane is fullscreen
3555 postScreenRebuild();
3556 repostHideMouse();
3558 receiverInit();
3560 MonoTime lastCollect = MonoTime.currTime;
3561 vbwin.eventLoop(1000*10,
3562 delegate () {
3563 egraProcessConsole();
3564 if (mainPane !is null) {
3565 if (mainPane.isViewChanged) postScreenRebuild();
3566 mainPane.checkSaveState();
3567 setupTrayAnimation();
3570 immutable ctt = MonoTime.currTime;
3571 if ((ctt-lastCollect).total!"minutes" >= 1) {
3572 import core.memory : GC;
3573 lastCollect = ctt;
3574 GC.collect();
3575 GC.minimize();
3579 delegate (KeyEvent event) {
3580 egraOnKey(event);
3581 if (mainPane !is null && mainPane.isViewChanged) postScreenRebuild();
3583 delegate (MouseEvent event) {
3584 egraOnMouse(event);
3585 if (mainPane !is null && mainPane.isViewChanged) postScreenRebuild();
3587 delegate (dchar ch) {
3588 egraOnChar(ch);
3589 if (mainPane !is null && mainPane.isViewChanged) postScreenRebuild();
3593 mainPane.saveCurrentState();
3595 trayimages[] = null;
3596 if (trayicon !is null && !trayicon.closed) { trayicon.close(); trayicon = null; }
3597 flushGui();
3598 receiverDeinit();
3599 conProcessQueue(int.max/4);