use `gxWithSavedClip()` instead of clip saver
[chiroptera.git] / chiroptera.d
blob1eb8e447dbe115baa117b67dd8dd54852106e49a
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 = article_can_into_html;
21 import core.atomic;
22 import core.time;
23 import std.concurrency;
25 //import arsd.email;
26 //import arsd.htmltotext;
27 import arsd.simpledisplay;
28 import arsd.png;
30 version(article_can_into_html) {
31 import arsd.characterencodings;
32 import arsd.color;
33 import arsd.dom;
34 import arsd.htmltotext;
37 import iv.alice;
38 import iv.bclamp;
39 import iv.encoding;
40 import iv.cmdcon;
41 import iv.cmdcongl;
42 import iv.lockfile;
43 import iv.sdpyutil;
44 import iv.strex;
45 import iv.sq3;
46 import iv.timer : DurTimer = Timer;
47 import iv.utfutil;
48 import iv.vfs.io;
49 import iv.vfs.util;
51 import egfx;
52 import egui;
54 import chibackend;
55 import chibackend.net;
57 import chievents;
58 import dialogs;
59 import receiver;
62 // ////////////////////////////////////////////////////////////////////////// //
63 private __gshared bool ChiroTimerExEnabled = false;
66 // ////////////////////////////////////////////////////////////////////////// //
67 public __gshared string mailRootDir = "/mnt/bigass/Mail";
70 shared static this () {
71 import core.stdc.stdlib : getenv;
72 const(char)* home = getenv("HOME");
73 if (home !is null && home[0] == '/' && home[1] && home[1] != '/') {
74 import std.string : fromStringz;
75 mailRootDir = home.fromStringz.idup;
76 if (mailRootDir.length == 0) assert(0, "wtf?!");
77 if (mailRootDir[$-1] != '/') mailRootDir ~= "/";
78 mailRootDir ~= "Mail";
83 // ////////////////////////////////////////////////////////////////////////// //
84 __gshared ubyte vbufNewScale = 1; // new window scale
85 __gshared int lastWinWidth, lastWinHeight;
86 __gshared bool vbufVSync = false;
87 __gshared bool vbufEffVSync = false;
89 __gshared NotificationAreaIcon trayicon;
90 __gshared Image[6] trayimages;
91 __gshared MemoryImage[6] icons; // 0: normal
94 // ////////////////////////////////////////////////////////////////////////// //
95 // with tagid
96 struct ArticleId {
97 uint tagid = 0;
98 uint uid = 0;
100 bool valid () const nothrow @safe @nogc { pragma(inline, true); return (tagid && uid); }
101 void clear () nothrow @safe @nogc { pragma(inline, true); tagid = uid = 0; }
103 bool opEqual (const ref ArticleId other) const nothrow @safe @nogc {
104 pragma(inline, true);
105 return (valid && other.valid && tagid == other.tagid && uid == other.uid);
110 // ////////////////////////////////////////////////////////////////////////// //
111 static immutable ubyte[] iconsZipData = cast(immutable(ubyte)[])import("databin/icons.zip");
114 // ////////////////////////////////////////////////////////////////////////// //
115 struct MailReplacement {
116 string oldmail;
117 string newmail;
118 string newname;
121 __gshared MailReplacement[string] repmailreps;
124 // ////////////////////////////////////////////////////////////////////////// //
125 string getBrowserCommand (bool forceOpera) {
126 __gshared string browser;
127 if (forceOpera) return "opera";
128 if (browser.length == 0) {
129 import core.stdc.stdlib : getenv;
130 const(char)* evar = getenv("BROWSER");
131 if (evar !is null && evar[0]) {
132 import std.string : fromStringz;
133 browser = evar.fromStringz.idup;
134 } else {
135 browser = "opera";
138 return browser;
142 // ////////////////////////////////////////////////////////////////////////// //
144 private void setXStr (ref char[] dest, SQ3Text src) {
145 delete dest;
146 if (src.length == 0) return;
147 dest = new char[src.length];
148 dest[] = src[];
153 // ////////////////////////////////////////////////////////////////////////// //
154 class FolderInfo {
155 uint tagid; // 0 means "ephemeral"
156 DynStr name;
157 const(char)[] visname; // slice of the `name`
158 int depth;
159 uint unreadCount;
160 // used in rescanner;
161 bool seen;
163 ~this () nothrow @trusted @nogc { clear(); }
165 void clear () nothrow @trusted @nogc {
166 visname = null;
167 name.clear();
170 // ephemeral folders doesn't exist, they are here only for visual purposes
171 bool ephemeral () const nothrow @safe @nogc { pragma(inline, true); return (tagid == 0); }
173 void calcDepthVisName () {
174 depth = 0;
175 visname = name[0..$];
176 if (visname.length == 0 || visname == "/" || visname[0] == '#') return;
177 visname = visname[1..$];
178 foreach (immutable char ch; visname) if (ch == '/') ++depth;
179 auto spos = visname.lastIndexOf('/');
180 if (spos >= 0) visname = visname[spos+1..$];
183 static int findByFullName (const(char)[] aname) {
184 if (aname.length == 0) return -1;
185 foreach (immutable idx, const FolderInfo fi; folderList) {
186 if (fi.name == aname) return cast(int)idx;
188 return -1;
191 bool needEphemeral () const {
192 if (depth == 0) return false;
193 assert(name.length > 1 && name[0] == '/');
194 const(char)[] n = name[0..$];
195 while (n.length) {
196 if (findByFullName(n) < 0) return true;
197 auto spos = n.lastIndexOf('/');
198 if (spos <= 0) return false;
199 n = n[0..spos];
201 return false;
204 void createEphemerals () const {
205 if (depth == 0) return;
206 assert(name.length > 1 && name[0] == '/');
207 const(char)[] n = name[0..$];
208 while (n.length) {
209 auto spos = n.lastIndexOf('/');
210 if (spos <= 0) break;
211 n = n[0..spos];
212 //conwriteln(" n=<", n, ">; spos=", spos);
213 if (findByFullName(n) < 0) {
214 //conwriteln(" creating: '", n, "'");
215 //foreach (const FolderInfo nfi; folderList) conwriteln(" <", nfi.name, "> : <", nfi.visname, "> : ", nfi.depth);
216 FolderInfo newfi = new FolderInfo;
217 newfi.tagid = 0;
218 newfi.name = n;
219 newfi.unreadCount = 0;
220 newfi.seen = true;
221 newfi.calcDepthVisName();
222 folderList ~= newfi;
228 __gshared FolderInfo[] folderList;
231 //FIXME: make this faster
232 //FIXME: force-append account folders
233 // returns `true` if something was changed
234 bool rescanFolders () {
235 import std.conv : to;
236 bool res = false;
238 bool needsort = false;
239 foreach (FolderInfo fi; folderList) fi.seen = false;
241 // unhide any folder tags with unread messages
242 static stmtUnhide = LazyStatement!"View"(`
243 UPDATE tagnames
245 hidden=0
246 WHERE
247 hidden=1 AND
248 (tag='#spam' OR (tag<>'' AND SUBSTR(tag, 1, 1)='/')) AND
249 EXISTS (SELECT uid FROM threads WHERE tagid=tagnames.tagid AND appearance=`~(cast(int)Appearance.Unread).to!string~`)
250 ;`);
251 stmtUnhide.st.doAll();
253 static auto stmtGet = LazyStatement!"View"(`
254 SELECT
255 tagid AS tagid
256 , tag AS name
257 FROM tagnames
258 WHERE
259 tag='#spam' OR
260 (hidden=0 AND tag<>'' AND SUBSTR(tag, 1, 1)='/')
261 ;`);
263 static auto stmtGetUnread = LazyStatement!"View"(`
264 SELECT
265 COUNT(uid) AS unread
266 FROM threads
267 WHERE tagid=:tagid AND appearance=`~(cast(int)Appearance.Unread).to!string~`
268 ;`);
270 foreach (auto row; stmtGet.st.range) {
271 bool append = true;
272 uint tagid = row.tagid!uint;
273 foreach (FolderInfo fi; folderList) {
274 if (fi.tagid == tagid) {
275 append = false;
276 fi.seen = true;
277 if (fi.name != row.name!SQ3Text) {
278 fi.name = row.name!SQ3Text;
279 fi.calcDepthVisName();
280 needsort = true;
282 break;
285 if (append) {
286 needsort = true;
287 FolderInfo newfi = new FolderInfo();
288 newfi.tagid = tagid;
289 newfi.name = row.name!SQ3Text;
290 newfi.unreadCount = 0;
291 newfi.seen = true;
292 newfi.calcDepthVisName();
293 folderList ~= newfi;
297 // remove unseen folders
298 for (usize f = 0; f < folderList.length; ) {
299 if (!folderList[f].seen && !folderList[f].ephemeral) {
300 needsort = true;
301 folderList[f].clear();
302 delete folderList[f];
303 foreach (immutable c; f+1..folderList.length) folderList[c-1] = folderList[c];
304 folderList[$-1] = null;
305 folderList.length -= 1;
306 } else {
307 ++f;
311 if (needsort) {
312 // remove all epemerals
313 for (usize f = 0; f < folderList.length; ) {
314 if (folderList[f].ephemeral) {
315 folderList[f].clear();
316 delete folderList[f];
317 foreach (immutable c; f+1..folderList.length) folderList[c-1] = folderList[c];
318 folderList[$-1] = null;
319 folderList.length -= 1;
320 } else {
321 ++f;
325 // readd all epemerals
326 for (;;) {
327 bool again = false;
328 foreach (FolderInfo fi; folderList) {
329 if (fi.needEphemeral) {
330 //conwriteln("ephemeral for '", fi.name, "'");
331 again = true;
332 fi.createEphemerals();
333 break;
336 if (!again) break;
339 static bool isAccount (const(char)[] s) pure nothrow @trusted @nogc {
340 if (!s.startsWith("/accounts")) return false;
341 return (s.length == 9 || s[9] == '/');
344 import std.algorithm.sorting : sort;
345 folderList.sort!((const FolderInfo a, const FolderInfo b) {
346 if (a.name == b.name) return false;
347 if (isAccount(a.name) && !isAccount(b.name)) return true; // a < b
348 if (!isAccount(a.name) && isAccount(b.name)) return false; // a >= b
349 if (a.name[0] == '#' && b.name[0] != '#') return false; // a >= b
350 if (a.name[0] != '#' && b.name[0] == '#') return true; // a < b
351 return (a.name < b.name);
353 res = true;
356 // check unread counts
357 foreach (FolderInfo fi; folderList) {
358 if (fi.ephemeral) continue;
359 foreach (auto row; stmtGetUnread.st.bind(":tagid", fi.tagid).range) {
360 if (fi.unreadCount != row.unread!uint) {
361 res = true;
362 fi.unreadCount = row.unread!uint;
367 setupTrayAnimation();
368 return res;
372 // ////////////////////////////////////////////////////////////////////////// //
373 __gshared bool dbg_dump_keynames;
376 // ////////////////////////////////////////////////////////////////////////// //
377 class TrayAnimationStepEvent {}
378 __gshared TrayAnimationStepEvent evTrayAnimationStep;
379 shared static this () { evTrayAnimationStep = new TrayAnimationStepEvent(); }
381 __gshared int trayAnimationIndex = 0; // 0: no animation
382 __gshared int trayAnimationDir = 1; // direction
385 // ////////////////////////////////////////////////////////////////////////// //
386 void trayPostAnimationEvent () {
387 if (vbwin !is null && !vbwin.eventQueued!TrayAnimationStepEvent) vbwin.postTimeout(evTrayAnimationStep, 100);
391 void trayDoAnimationStep () {
392 if (trayicon is null || trayicon.closed) return; // no tray icon
393 if (vbwin is null || vbwin.closed) return;
394 if (trayAnimationIndex == 0) return; // no animation
395 trayPostAnimationEvent();
396 if (trayAnimationDir < 0) {
397 if (--trayAnimationIndex == 1) trayAnimationDir = 1;
398 } else {
399 if (++trayAnimationIndex == trayimages.length-1) trayAnimationDir = -1;
401 trayicon.icon = trayimages[trayAnimationIndex];
402 //vbwin.icon = icons[trayAnimationIndex];
403 //vbwin.sendDummyEvent(); // or it won't redraw itself
404 //flushGui(); // or it may not redraw itself
408 // ////////////////////////////////////////////////////////////////////////// //
409 void trayStartAnimation () {
410 if (trayicon is null) return; // no tray icon
411 if (trayAnimationIndex == 0) {
412 trayAnimationIndex = 1;
413 trayAnimationDir = 1;
414 trayicon.icon = trayimages[1];
415 vbwin.icon = icons[1];
416 flushGui(); // or it may not redraw itself
417 trayPostAnimationEvent();
422 void trayStopAnimation () {
423 if (trayicon is null) return; // no tray icon
424 if (trayAnimationIndex != 0) {
425 trayAnimationIndex = 0;
426 trayAnimationDir = 1;
427 trayicon.icon = trayimages[0];
428 vbwin.icon = icons[0];
429 flushGui(); // or it may not redraw itself
434 // check if we have to start/stop animation, and do it
435 void setupTrayAnimation () {
436 import std.conv : to;
437 static auto stmtGetUnread = LazyStatement!"View"(`
438 SELECT 1
439 FROM threads
440 WHERE appearance=`~(cast(int)Appearance.Unread).to!string~`
441 LIMIT 1
442 ;`);
444 foreach (auto row; stmtGetUnread.st.range) {
445 trayStartAnimation();
446 return;
448 trayStopAnimation();
452 // ////////////////////////////////////////////////////////////////////////// //
453 __gshared string[string] mainAppKeyBindings;
455 void clearBindings () {
456 mainAppKeyBindings.clear();
460 void mainAppBind (ConString kname, ConString concmd) {
461 KeyEvent evt = KeyEvent.parse(kname);
462 if (concmd.length) {
463 mainAppKeyBindings[evt.toStr] = concmd.idup;
464 } else {
465 mainAppKeyBindings.remove(evt.toStr);
470 void mainAppUnbind (ConString kname) {
471 KeyEvent evt = KeyEvent.parse(kname);
472 mainAppKeyBindings.remove(evt.toStr);
476 void setupDefaultBindings () {
477 //mainAppBind("C-L", "dbg_font_window");
478 mainAppBind("C-Q", "quit_prompt");
480 mainAppBind("N", "next_unread ona");
481 mainAppBind("S-N", "next_unread tan");
482 mainAppBind("M", "next_unread ona");
483 mainAppBind("S-M", "next_unread tan");
485 mainAppBind("U", "mark_unread");
486 mainAppBind("R", "mark_read");
488 mainAppBind("Space", "artext_page_down");
489 mainAppBind("S-Space", "artext_page_up");
490 mainAppBind("M-Up", "artext_line_up");
491 mainAppBind("M-Down", "artext_line_down");
493 mainAppBind("Up", "article_prev");
494 mainAppBind("Down", "article_next");
495 mainAppBind("PageUp", "article_pgup");
496 mainAppBind("PageDown", "article_pgdown");
497 mainAppBind("Home", "article_to_first");
498 mainAppBind("End", "article_to_last");
499 mainAppBind("C-Up", "article_scroll_up");
500 mainAppBind("C-Down", "article_scroll_down");
502 mainAppBind("C-PageUp", "folder_prev");
503 mainAppBind("C-PageDown", "folder_next");
505 mainAppBind("M-O", "folder_options");
507 //mainAppBind("C-M-U", "folder_update");
508 mainAppBind("C-S-I", "update_all");
509 mainAppBind("C-H", "article_dump_headers");
511 mainAppBind("C-Backslash", "find_mine");
513 //mainAppBind("C-Slash", "article_to_parent");
514 //mainAppBind("C-Comma", "article_to_prev_sib");
515 //mainAppBind("C-Period", "article_to_next_sib");
517 //mainAppBind("C-Insert", "article_copy_url_to_clipboard");
518 mainAppBind("C-M-K", "article_twit_thread");
519 mainAppBind("T", "article_edit_poster_title");
521 mainAppBind("C-R", "article_reply");
522 mainAppBind("S-R", "article_reply_to_from");
524 mainAppBind("S-P", "new_post");
526 //mainAppBind("S-Enter", "article_open_in_browser");
527 //mainAppBind("M-Enter", "article_open_in_browser tan");
529 mainAppBind("Delete", "article_softdelete_toggle");
530 mainAppBind("C-Delete", "article_harddelete_toggle");
534 // ////////////////////////////////////////////////////////////////////////// //
535 struct ImgViewCommand {
536 string filename;
539 private void imageViewThread (Tid ownerTid) {
540 string fname;
541 try {
542 conwriteln("waiting for the message...");
543 receive(
544 (ImgViewCommand cmd) {
545 fname = cmd.filename;
548 conwriteln("got filename: \"", fname, "\"");
550 try {
551 import std.process;
552 //FIXME: make regvar for image viewer
553 //auto pid = execute(["keh", attfname], null, Config.detached);
554 //pid.wait();
555 import std.stdio : File;
556 auto frd = File("/dev/null");
557 auto fwr = File("/dev/null", "w");
558 auto pid = spawnProcess(["keh", fname], frd, fwr, fwr, null, Config.none/*detached*/);
559 pid.wait();
560 } catch (Exception e) {
561 conwriteln("ERROR executing image viewer: ", e.msg);
563 } catch (Throwable e) {
564 // here, we are dead and fucked (the exact order doesn't matter)
565 //import core.stdc.stdlib : abort;
566 import core.stdc.stdio : fprintf, stderr;
567 //import core.memory : GC;
568 import core.thread : thread_suspendAll;
569 //GC.disable(); // yeah
570 //thread_suspendAll(); // stop right here, you criminal scum!
571 auto s = e.toString();
572 fprintf(stderr, "\n=== FATAL ===\n%.*s\n", cast(uint)s.length, s.ptr);
574 try {
575 import std.file : remove;
576 if (fname.length) {
577 conwriteln("deleting file \"", fname, "\"");
578 remove(fname);
580 } catch (Exception e) {}
584 // ////////////////////////////////////////////////////////////////////////// //
585 void initConsole () {
586 import std.functional : toDelegate;
588 //conRegVar!vbufNewScale(1, 4, "v_scale", "window scale: [1..3]");
590 conRegVar!bool("v_vsync", "sync to video refresh rate?",
591 (ConVarBase self) => vbufVSync,
592 (ConVarBase self, bool nv) {
593 static if (EGfxOpenGLBackend) {
594 if (vbufVSync != nv) {
595 vbufVSync = nv;
596 postScreenRepaint();
602 conRegVar!scrollKeepLines("scroll_keep_lines", "number of lines to keep on page up or page down.");
603 conRegVar!unreadTimeoutInterval("t_unread_timeout", "timout to mark keyboard-selected message as unread (<1: don't mark)");
605 conRegFunc!clearBindings("binds_app_clear", "clear main application keybindings");
606 conRegFunc!setupDefaultBindings("binds_app_default", "*append* default application bindings");
607 conRegFunc!mainAppBind("bind_app", "add main application binding");
608 conRegFunc!mainAppUnbind("unbind_app", "remove main application binding");
611 // //////////////////////////////////////////////////////////////////// //
612 conRegFunc!(() {
613 import core.memory : GC;
614 conwriteln("starting GC collection...");
615 GC.collect();
616 GC.minimize();
617 conwriteln("GC collection complete.");
618 })("gc_collect", "force GC collection cycle");
621 // //////////////////////////////////////////////////////////////////// //
622 conRegFunc!(() {
623 auto qww = new YesNoWindow("Quit?", "Do you really want to quit?", true);
624 qww.onYes = () { concmd("quit"); };
625 qww.addModal();
626 })("quit_prompt", "quit with prompt");
629 // //////////////////////////////////////////////////////////////////// //
630 conRegFunc!((ConString url, bool forceOpera=false) {
631 if (url.length) {
632 import std.stdio : File;
633 import std.process;
634 try {
635 auto frd = File("/dev/null");
636 auto fwr = File("/dev/null", "w");
637 spawnProcess([getBrowserCommand(forceOpera), url.idup], frd, fwr, fwr, null, Config.detached);
638 } catch (Exception e) {
639 conwriteln("ERROR executing URL viewer (", e.msg, ")");
642 })("open_url", "open given url in a browser");
645 // //////////////////////////////////////////////////////////////////// //
646 conRegFunc!(() {
647 receiverForceUpdateAll();
648 })("update_all", "mark all groups for updating");
651 // //////////////////////////////////////////////////////////////////// //
652 conRegFunc!(() {
653 if (mainPane !is null && mainPane.folderUpOne()) {
654 postScreenRebuild();
656 })("folder_prev", "go to previous group");
658 conRegFunc!(() {
659 if (mainPane !is null && mainPane.folderDownOne()) {
660 postScreenRebuild();
662 })("folder_next", "go to next group");
665 // //////////////////////////////////////////////////////////////////// //
666 conRegFunc!(() {
667 if (vbwin is null || vbwin.closed) return;
668 if (mainPane is null) return;
669 if (chiroGetMessageExactRead(mainPane.lastDecodedTid, mainPane.msglistCurrUId)) {
670 chiroSetMessageUnread(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
671 postScreenRebuild();
672 setupTrayAnimation();
674 })("mark_unread", "mark current message as unread");
676 conRegFunc!(() {
677 if (vbwin is null || vbwin.closed) return;
678 if (mainPane is null) return;
679 if (chiroGetMessageUnread(mainPane.lastDecodedTid, mainPane.msglistCurrUId)) {
680 chiroSetMessageRead(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
681 setupTrayAnimation();
682 postScreenRebuild();
684 })("mark_read", "mark current message as read");
686 conRegFunc!((bool allowNextGroup=false) {
687 if (vbwin is null || vbwin.closed) return;
688 if (mainPane is null) return;
689 // try current group
690 if (mainPane.lastDecodedTid != 0) {
691 auto uid = chiroGetPaneNextUnread(mainPane.msglistCurrUId);
692 if (uid) {
693 // i found her!
694 if (uid == mainPane.msglistCurrUId) return;
695 mainPane.msglistCurrUId = uid;
696 mainPane.threadListPositionDirty();
697 chiroSetMessageRead(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
698 setupTrayAnimation();
699 postScreenRebuild();
700 return;
703 // try other groups?
704 if (!allowNextGroup) return;
705 int idx = mainPane.folderCurrIndex;
706 foreach (; 0..cast(int)folderList.length) {
707 idx = (idx+1)%cast(int)folderList.length;
708 if (idx == mainPane.folderCurrIndex) continue;
709 if (folderList[idx].ephemeral) continue;
710 if (folderList[idx].unreadCount == 0) continue;
711 mainPane.folderSetToIndex(idx);
712 auto uid = chiroGetPaneNextUnread(/*mainPane.msglistCurrUId*/0);
713 if (uid) {
714 // i found her!
715 if (uid == mainPane.msglistCurrUId) return;
716 mainPane.msglistCurrUId = uid;
717 mainPane.threadListPositionDirty();
718 chiroSetMessageRead(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
719 setupTrayAnimation();
720 postScreenRebuild();
721 return;
724 })("next_unread", "move to next unread message; bool arg: can skip to next group?");
727 // //////////////////////////////////////////////////////////////////// //
728 conRegFunc!(() {
729 if (mainPane !is null) mainPane.scrollByPageUp();
730 })("artext_page_up", "do pageup on article text");
732 conRegFunc!(() {
733 if (mainPane !is null) mainPane.scrollByPageDown();
734 })("artext_page_down", "do pagedown on article text");
736 conRegFunc!(() {
737 if (mainPane !is null) mainPane.scrollBy(-1);
738 })("artext_line_up", "do lineup on article text");
740 conRegFunc!(() {
741 if (mainPane !is null) mainPane.scrollBy(1);
742 })("artext_line_down", "do linedown on article text");
744 // //////////////////////////////////////////////////////////////////// //
745 /*FIXME
746 conRegFunc!((bool forceOpera=false) {
747 if (auto fldx = getActiveFolder) {
748 fldx.withBaseReader((abase, cur, top, alist) {
749 if (cur < alist.length) {
750 abase.loadContent(alist[cur]);
751 if (auto art = abase[alist[cur]]) {
752 scope(exit) art.releaseContent();
753 auto path = art.getHeaderValue("path:");
754 //conwriteln("path: [", path, "]");
755 if (path.startsWithCI("digitalmars.com!.POSTED.")) {
756 import std.stdio : File;
757 import std.process;
758 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
759 string id = art.msgid;
760 if (id.length > 2 && id[0] == '<' && id[$-1] == '>') id = id[1..$-1];
761 auto pid = spawnProcess(
762 [getBrowserCommand(forceOpera), "http://forum.dlang.org/post/"~id],
763 File("/dev/zero"), File("/dev/null"), File("/dev/null"),
765 pid.wait();
771 })("article_open_in_browser", "open the current article in browser (dlang forum)");
774 /*FIXME
775 conRegFunc!(() {
776 if (auto fldx = getActiveFolder) {
777 fldx.withBaseReader((abase, cur, top, alist) {
778 if (cur < alist.length) {
779 if (auto art = abase[alist[cur]]) {
780 auto path = art.getHeaderValue("path:");
781 if (path.startsWithCI("digitalmars.com!.POSTED.")) {
782 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
783 string id = art.msgid;
784 if (id.length > 2 && id[0] == '<' && id[$-1] == '>') id = id[1..$-1];
785 id = "http://forum.dlang.org/post/"~id;
786 setClipboardText(vbwin, id);
787 setPrimarySelection(vbwin, id);
793 })("article_copy_url_to_clipboard", "copy the url of the current article to clipboard (dlang forum)");
797 // //////////////////////////////////////////////////////////////////// //
798 conRegFunc!((ConString oldmail, ConString newmail, ConString newname) {
799 if (oldmail.length) {
800 if (newmail.length == 0) {
801 repmailreps.remove(oldmail.idup);
802 } else {
803 MailReplacement mr;
804 mr.oldmail = oldmail.idup;
805 mr.newmail = newmail.idup;
806 mr.newname = newname.idup;
807 repmailreps[mr.oldmail] = mr;
810 })("append_replyto_mail_replacement", "append replacement for reply mails");
813 // //////////////////////////////////////////////////////////////////// //
814 conRegFunc!(() {
815 if (mainPane !is null && mainPane.threadListUp()) {
816 postScreenRebuild();
818 })("article_prev", "go to previous article");
820 conRegFunc!(() {
821 if (mainPane !is null && mainPane.threadListDown()) {
822 postScreenRebuild();
824 })("article_next", "go to next article");
826 conRegFunc!(() {
827 if (mainPane !is null && mainPane.threadListPageUp()) {
828 postScreenRebuild();
830 })("article_pgup", "artiles list: page up");
832 conRegFunc!(() {
833 if (mainPane !is null && mainPane.threadListPageDown()) {
834 postScreenRebuild();
836 })("article_pgdown", "artiles list: page down");
838 conRegFunc!(() {
839 if (mainPane !is null && mainPane.threadListScrollUp(movecurrent:false)) {
840 postScreenRebuild();
842 })("article_scroll_up", "scroll article list up");
844 conRegFunc!(() {
845 if (mainPane !is null && mainPane.threadListScrollDown(movecurrent:false)) {
846 postScreenRebuild();
848 })("article_scroll_down", "scroll article list up");
850 conRegFunc!(() {
851 if (mainPane !is null && mainPane.threadListHome()) {
852 postScreenRebuild();
854 })("article_to_first", "go to first article");
856 conRegFunc!(() {
857 if (mainPane !is null && mainPane.threadListEnd()) {
858 postScreenRebuild();
860 })("article_to_last", "go to last article");
863 // //////////////////////////////////////////////////////////////////// //
864 conRegFunc!(() {
865 /*FIXME
866 if (auto fld = getActiveFolder) {
867 auto postDg = delegate (Account acc) {
868 conwriteln("post with account '", acc.name, "' (", acc.mail, ")");
869 auto pw = new PostWindow();
870 pw.from.str = acc.realname~" <"~acc.mail~">";
871 pw.from.readonly = true;
872 if (auto nna = cast(NntpAccount)acc) {
873 pw.title = "Reply to NNTP "~nna.server~":"~nna.group;
874 pw.to.str = nna.group;
875 pw.to.readonly = true;
876 pw.activeWidget = pw.subj;
877 } else {
878 pw.title = "Reply from '"~acc.name~"' ("~acc.mail~")";
879 pw.to.str = "";
880 pw.activeWidget = pw.to;
882 pw.subj.str = "";
883 pw.acc = acc;
884 pw.fld = fld;
887 auto acc = fld.findAccountToPost();
888 if (acc is null) {
889 auto wacc = new SelectPopBoxWindow(defaultAcc);
890 wacc.onSelected = postDg;
891 } else {
892 postDg(acc);
893 acc = defaultAcc;
895 } else {
896 conwriteln("post: no active folder");
899 })("new_post", "post a new article or message");
902 // //////////////////////////////////////////////////////////////////// //
903 static string buildToStr (string name, string mail, ref string newfromname) {
904 if (auto mrp = mail in repmailreps) {
905 newfromname = mrp.newname;
906 return mrp.newname~" <"~mrp.newmail~">";
907 } else {
908 return name~" <"~mail~">";
912 static void doReply (string repfld) {
913 /*FIXME
914 if (auto fld = getActiveFolder) {
915 auto postDg = delegate (Account acc) {
916 conwriteln("reply with account '", acc.name, "' (", acc.mail, ")");
917 fld.withBaseReader((abase, cur, top, alist) {
918 if (cur < alist.length) {
919 auto aidx = alist.ptr[cur];
920 abase.loadContent(aidx);
921 if (auto art = abase[aidx]) {
922 assert(art.contentLoaded);
923 auto atext = art.getTextContent;
924 auto pw = new PostWindow();
925 pw.from.str = acc.realname~" <"~acc.mail~">";
926 pw.from.readonly = true;
927 string from = art.fromname;
929 if (auto nna = cast(NntpAccount)acc) {
930 pw.title = "Reply to NNTP "~nna.server~":"~nna.group;
931 pw.to.str = nna.group;
932 pw.to.readonly = true;
933 } else {
934 pw.title = "Reply from '"~acc.name~"' ("~acc.mail~")";
935 //pw.to.str = art.fromname~" <"~art.frommail~">";
936 string rs;
937 if (repfld != "From") {
938 auto rf = art.getHeaderValue(repfld);
939 if (rf) {
940 //rs = buildToStr(art.fromname, repfld, from);
941 if (art.getHeaderValue("List-Id").length) {
942 rs = rf.idup;
943 } else {
944 rs = art.fromname~" <"~rf.idup~">";
948 if (!rs) rs = buildToStr(art.fromname, art.frommail, from);
949 pw.to.str = rs;
951 pw.subj.str = "Re: "~art.subj;
954 auto vp = from.indexOf(" via Digitalmars-");
955 if (vp > 0) {
956 from = from[0..vp].xstrip;
957 if (from.length == 0) from = "anonymous";
961 pw.ed.addText(from);
962 pw.ed.addText(" wrote:\n");
963 pw.ed.addText("\n");
964 foreach (ConString s; LineIterator!false(atext)) {
965 if (s.length == 0 || s[0] == '>') pw.ed.addText(">"); else pw.ed.addText("> ");
966 pw.ed.addText(s);
967 pw.ed.addText("\n");
969 pw.ed.addText("\n");
970 pw.ed.reformat();
971 pw.replyto = art.msgid;
972 pw.references = art.getHeaderValue("References").xstrip.idup;
973 pw.acc = acc;
974 pw.fld = fld;
975 pw.activeWidget = pw.ed;
981 auto acc = fld.findAccountToPost(fld.curidx);
982 if (acc is null) {
983 auto wacc = new SelectPopBoxWindow(defaultAcc);
984 wacc.onSelected = postDg;
985 } else {
986 postDg(acc);
987 acc = defaultAcc;
993 conRegFunc!(() { doReply("Reply-To"); })("article_reply", "reply to the current article with \"reply-to\" field");
994 conRegFunc!(() { doReply("From"); })("article_reply_to_from", "reply to the current article with \"from\" field");
997 // //////////////////////////////////////////////////////////////////// //
998 conRegFunc!(() {
999 if (vbwin is null || vbwin.closed) return;
1000 if (mainPane is null) return;
1001 if (mainPane.msglistCurrUId) {
1002 //messageBogoMarkHam(mainPane.msglistCurrUId);
1003 foreach (auto mrow; dbStore.statement(`SELECT ChiroUnpack(data) AS data FROM messages WHERE uid=:uid LIMIT 1;`).bind(":uid", mainPane.msglistCurrUId).range) {
1004 try {
1005 import core.stdc.stdio : snprintf;
1006 char[128] fname = void;
1007 auto len = snprintf(fname.ptr, fname.length, "~/Mail/_bots/z_eml_%u.eml", cast(uint)mainPane.msglistCurrUId);
1008 auto fo = VFile(fname[0..len], "w");
1009 fo.writeln(mrow.data!SQ3Text.xstripright);
1010 fo.close();
1011 conwriteln("article exported to: ", fname[0..len]);
1012 } catch (Exception e) {
1016 })("article_export", "export current article as raw text");
1019 conRegFunc!(() {
1020 if (vbwin is null || vbwin.closed) return;
1021 if (mainPane is null) return;
1022 if (mainPane.msglistCurrUId) {
1023 messageBogoMarkHam(mainPane.msglistCurrUId);
1024 //TODO: move out of spam
1026 })("article_mark_ham", "mark current article as ham");
1029 conRegFunc!(() {
1030 if (vbwin is null || vbwin.closed) return;
1031 if (mainPane is null) return;
1032 if (mainPane.msglistCurrUId) {
1033 immutable uint uid = mainPane.msglistCurrUId;
1034 // move to the next message
1035 if (!mainPane.threadListDown()) mainPane.threadListUp();
1036 conwriteln("calling bogofilter...");
1037 messageBogoMarkSpam(uid);
1038 conwriteln("adding '#spam' tag");
1039 immutable bool updatePane = chiroMessageAddTag(uid, "#spam");
1040 conwriteln("removing other virtual folder tags...");
1041 DynStr[] tags;
1042 scope(exit) delete tags;
1043 tags.reserve(32);
1044 static auto stGetMsgTags = LazyStatement!"View"(`
1045 SELECT DISTINCT(tagid) AS tagid, tt.tag AS name
1046 FROM threads
1047 INNER JOIN tagnames AS tt USING(tagid)
1048 WHERE uid=:uid
1049 ;`);
1050 foreach (auto row; stGetMsgTags.st.bind(":uid", uid).range) {
1051 auto tag = row.name!SQ3Text;
1052 conwriteln(" tag: ", tag);
1053 if (tag.startsWith("/")) tags ~= DynStr(tag);
1055 foreach (ref DynStr tn; tags) {
1056 conwriteln("removing tag '", tn.getData, "'...");
1057 chiroMessageRemoveTag(uid, tn.getData);
1059 conwriteln("done marking as spam.");
1060 if (updatePane) {
1061 mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
1062 } else {
1063 // rescan folder
1064 mainPane.updateViewsIfTid(chiroGetTagUid("#spam"));
1066 postScreenRebuild();
1068 })("article_mark_spam", "mark current article as spam");
1071 // //////////////////////////////////////////////////////////////////// //
1072 conRegFunc!((ConString fldname) {
1073 if (vbwin is null || vbwin.closed) return;
1074 if (mainPane is null) return;
1075 if (!mainPane.msglistCurrUId) return;
1076 immutable uint uid = mainPane.msglistCurrUId;
1078 fldname = fldname.xstrip;
1079 while (fldname.length && fldname[$-1] == '/') fldname = fldname[0..$-1].xstrip;
1080 immutable bool isforced = (fldname.length && fldname[0] == '!');
1081 if (isforced) fldname = fldname[1..$];
1082 if (fldname.length == 0) {
1083 conwriteln("ERROR: cannot move to empty folder");
1084 return;
1086 if (fldname[0].isalnum) {
1087 conwriteln("ERROR: invalid folder name '", fldname, "'");
1088 return;
1091 uint tagid;
1092 if (!isforced) {
1093 tagid = chiroGetTagUid(fldname);
1094 } else {
1095 tagid = chiroAppendTag(fldname, hidden:(fldname[0] == '#' ? 1 : 0));
1097 if (!tagid) {
1098 conwriteln("ERROR: invalid folder name '", fldname, "'");
1099 return;
1102 immutable bool updatePane = chiroMessageAddTag(uid, fldname);
1103 DynStr[] tags;
1104 scope(exit) delete tags;
1105 tags.reserve(32);
1106 static auto stGetMsgTags = LazyStatement!"View"(`
1107 SELECT DISTINCT(tagid) AS tagid, tt.tag AS name
1108 FROM threads
1109 INNER JOIN tagnames AS tt USING(tagid)
1110 WHERE uid=:uid
1111 ;`);
1112 foreach (auto row; stGetMsgTags.st.bind(":uid", uid).range) {
1113 auto tag = row.name!SQ3Text;
1114 conwriteln(" tag: ", tag);
1115 if (tag.startsWith("account:")) continue;
1116 if (tag != fldname) tags ~= DynStr(tag);
1118 foreach (ref DynStr tn; tags) {
1119 conwriteln("removing tag '", tn.getData, "'...");
1120 chiroMessageRemoveTag(uid, tn.getData);
1122 if (updatePane) {
1123 mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
1124 } else {
1125 // rescan folder
1126 mainPane.updateViewsIfTid(chiroGetTagUid("#spam"));
1128 postScreenRebuild();
1129 })("article_move_to_folder", "move article to existing folder");
1132 // //////////////////////////////////////////////////////////////////// //
1133 version(none) {
1134 conRegFunc!(() {
1135 /*FIXME
1136 if (auto fld = getActiveFolder) {
1137 fld.moveToParent();
1138 postScreenRebuild();
1141 })("article_to_parent", "jump to parent article, if any");
1143 conRegFunc!(() {
1144 /*FIXME
1145 if (auto fld = getActiveFolder) {
1146 fld.moveToPrevSib();
1147 postScreenRebuild();
1150 })("article_to_prev_sib", "jump to previous sibling");
1152 conRegFunc!(() {
1153 /*FIXME
1154 if (auto fld = getActiveFolder) {
1155 fld.moveToNextSib();
1156 postScreenRebuild();
1159 })("article_to_next_sib", "jump to next sibling");
1162 // //////////////////////////////////////////////////////////////////// //
1163 conRegFunc!(() {
1164 if (vbwin is null || vbwin.closed) return;
1165 if (mainPane is null) return;
1166 if (mainPane.lastDecodedTid == 0 || mainPane.msglistCurrUId == 0) return;
1167 DynStr tagname = chiroGetTagName(mainPane.lastDecodedTid);
1168 if (!globmatch(tagname.getData, "/dmars_ng/*")) return;
1169 // get from
1170 DynStr fromMail, fromName;
1171 if (!chiroGetMessageFrom(mainPane.msglistCurrUId, ref fromMail, ref fromName)) return;
1172 if (fromMail.length == 0 && fromName.length == 0) return;
1173 //writeln("!!! email=", fromMail.getData, "; name=", fromName.getData, "|");
1175 uint twitid = 0;
1176 DynStr twtitle;
1177 DynStr twname;
1178 DynStr twemail;
1179 DynStr twnotes;
1180 DynStr twtagglob = "/dmars_ng/*";
1181 bool withName = false;
1183 static auto stFindTwit = LazyStatement!"Conf"(`
1184 SELECT
1185 etwitid AS twitid
1186 , tagglob AS tagglob
1187 , email AS email
1188 , name AS name
1189 , title AS title
1190 , notes AS notes
1191 FROM emailtwits
1192 WHERE email=:email AND name=:name
1193 ;`);
1195 static auto stAddTwit = LazyStatement!"Conf"(`
1196 INSERT INTO emailtwits
1197 ( tagglob, email, name, title, notes)
1198 VALUES(:tagglob,:email,:name,:title,:notes)
1199 ;`);
1201 static auto stModifyTwit = LazyStatement!"Conf"(`
1202 UPDATE emailtwits
1204 tagglob=:tagglob
1205 , email=:email
1206 , name=:name
1207 , title=:title
1208 , notes=:notes
1209 WHERE etwitid=:twitid
1210 ;`);
1212 static auto stRemoveTwitAuto = LazyStatement!"Conf"(`
1213 DELETE FROM msgidtwits
1214 WHERE etwitid=:twitid
1215 ;`);
1217 static auto stRemoveTwit = LazyStatement!"Conf"(`
1218 DELETE FROM emailtwits
1219 WHERE etwitid=:twitid
1220 ;`);
1222 stFindTwit.st
1223 .bindConstText(":email", fromMail.getData)
1224 .bindConstText(":name", fromName.getData);
1225 foreach (auto row; stFindTwit.st.range) {
1226 conwriteln("!!!", row.twitid!uint, "; mail=", row.email!SQ3Text, "; name=", row.name!SQ3Text, "; title=", row.title!SQ3Text.recodeToKOI8);
1227 if (!globmatch(tagname.getData, row.tagglob!SQ3Text)) continue;
1228 twitid = row.twitid!uint;
1229 twtitle = row.title!SQ3Text;
1230 twname = row.name!SQ3Text;
1231 twemail = row.email!SQ3Text;
1232 twtagglob = row.tagglob!SQ3Text;
1233 twnotes = row.notes!SQ3Text;
1234 withName = (fromName.length != 0);
1235 break;
1238 if (!twitid && fromName.length) {
1239 stFindTwit.st
1240 .bindConstText(":email", fromMail.getData)
1241 .bindConstText(":name", "");
1242 foreach (auto row; stFindTwit.st.range) {
1243 conwriteln("!!!", row.twitid!uint, "; mail=", row.email!SQ3Text, "; name=", row.name!SQ3Text, "; title=", row.title!SQ3Text.recodeToKOI8);
1244 if (!globmatch(tagname.getData, row.tagglob!SQ3Text)) continue;
1245 twitid = row.twitid!uint;
1246 twtitle = row.title!SQ3Text;
1247 twname = row.name!SQ3Text;
1248 twemail = row.email!SQ3Text;
1249 twtagglob = row.tagglob!SQ3Text;
1250 twnotes = row.notes!SQ3Text;
1251 withName = false;
1252 break;
1256 conwriteln("twitid: ", twitid, "; title=", twtitle.getData.recodeToKOI8);
1258 auto tw = new TitlerWindow((twitid ? twname : fromName), (twitid ? twemail : fromMail), twtagglob, twtitle);
1259 tw.onSelected = delegate (name, email, glob, title) {
1260 if (email.length == 0 && name.length == 0) return false;
1261 if (glob.length == 0) return false;
1262 if (email.length && email[0] == '@') return false;
1263 title = title.xstrip;
1264 if (twitid) {
1265 if (title.length == 0) {
1266 // remove twit
1267 conwriteln("removing twit...");
1268 stRemoveTwitAuto.st.bind(":twitid", twitid).doAll();
1269 stRemoveTwit.st.bind(":twitid", twitid).doAll();
1270 } else {
1271 // change twit
1272 conwriteln("changing twit...");
1273 stModifyTwit.st
1274 .bind(":twitid", twitid)
1275 .bindConstText(":tagglob", glob)
1276 .bindConstText(":email", email)
1277 .bindConstText(":name", name)
1278 .bindConstText(":title", title)
1279 .bindConstText(":notes", twnotes.getData, allowNull:true)
1280 .doAll();
1282 } else {
1283 if (title.length == 0) return false;
1284 // new twit
1285 conwriteln("adding twit...");
1286 stAddTwit.st
1287 .bindConstText(":tagglob", glob)
1288 .bindConstText(":email", email)
1289 .bindConstText(":name", name)
1290 .bindConstText(":title", title)
1291 .bindConstText(":notes", null, allowNull:true)
1292 .doAll();
1295 if (vbwin && !vbwin.closed) vbwin.postEvent(new RecalcAllTwitsEvent());
1296 return true;
1298 })("article_edit_poster_title", "edit poster's title of the current article");
1301 // //////////////////////////////////////////////////////////////////// //
1302 conRegFunc!(() {
1303 if (vbwin is null || vbwin.closed) return;
1304 if (mainPane is null) return;
1305 if (mainPane.lastDecodedTid == 0 || mainPane.msglistCurrUId == 0) return;
1306 DynStr tagname = chiroGetTagName(mainPane.lastDecodedTid);
1307 if (!globmatch(tagname.getData, "/dmars_ng/*")) return;
1308 int mute = chiroGetMessageMute(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
1309 // check for non-automatic mutes
1310 if (mute != Mute.Normal && mute <= Mute.ThreadStart) return;
1311 createTwitByMsgid(mainPane.msglistCurrUId);
1312 mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
1313 postScreenRebuild();
1314 })("article_twit_thread", "twit current thread");
1316 conRegFunc!(() {
1317 if (vbwin is null || vbwin.closed) return;
1318 if (mainPane is null) return;
1319 int app = chiroGetMessageAppearance(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
1320 if (app >= 0) {
1321 //conwriteln("oldapp: ", app, " (", isSoftDeleted(app), ")");
1322 immutable bool wasPurged = (app == Appearance.SoftDeletePurge);
1323 app =
1324 app == Appearance.SoftDeletePurge ? Appearance.SoftDeleteUser :
1325 isSoftDeleted(app) ? Appearance.Read :
1326 Appearance.SoftDeleteUser;
1327 //conwriteln("newapp: ", app);
1328 chiroSetMessageAppearance(mainPane.lastDecodedTid, mainPane.msglistCurrUId, cast(Appearance)app);
1329 if (!wasPurged && isSoftDeleted(app)) mainPane.threadListDown();
1330 postScreenRebuild();
1332 })("article_softdelete_toggle", "toggle \"soft deleted\" flag on current article");
1334 conRegFunc!(() {
1335 if (vbwin is null || vbwin.closed) return;
1336 if (mainPane is null) return;
1337 int app = chiroGetMessageAppearance(mainPane.lastDecodedTid, mainPane.msglistCurrUId);
1338 if (app >= 0) {
1339 //conwriteln("oldapp: ", app);
1340 app =
1341 app == Appearance.SoftDeletePurge ? Appearance.Read :
1342 isSoftDeleted(app) ? Appearance.SoftDeletePurge :
1343 Appearance.SoftDeletePurge;
1344 //conwriteln("newapp: ", app);
1345 chiroSetMessageAppearance(mainPane.lastDecodedTid, mainPane.msglistCurrUId, cast(Appearance)app);
1346 if (app == Appearance.SoftDeletePurge) mainPane.threadListDown();
1347 postScreenRebuild();
1349 })("article_harddelete_toggle", "toggle \"hard deleted\" flag on current article");
1352 // //////////////////////////////////////////////////////////////////// //
1353 conRegFunc!(() {
1354 if (vbwin is null || vbwin.closed) return;
1355 if (mainPane is null) return;
1356 DynStr hdrs = chiroMessageHeaders(mainPane.msglistCurrUId);
1357 if (hdrs.length == 0) return;
1358 conwriteln("============================");
1359 conwriteln("message uid: ", mainPane.msglistCurrUId);
1360 conwriteln(" tag tagid: ", mainPane.lastDecodedTid);
1361 conwriteln("============================");
1362 forEachHeaderLine(hdrs.getData, (const(char)[] line) {
1363 conwriteln(" ", line.xstripright);
1364 return true; // go on
1366 })("article_dump_headers", "dump article headers");
1368 conRegFunc!(() {
1369 if (vbwin is null || vbwin.closed) return;
1370 if (mainPane is null) return;
1371 DynStr hdrs = chiroMessageHeaders(mainPane.msglistCurrUId);
1372 if (hdrs.length == 0) return;
1373 conwriteln("============================");
1374 conwriteln("message uid: ", mainPane.msglistCurrUId);
1375 conwriteln(" tag tagid: ", mainPane.lastDecodedTid);
1376 conwriteln("============================");
1377 auto bogo = messageBogoCheck(mainPane.msglistCurrUId);
1378 conwriteln("BOGO RESULT: ", bogo);
1379 })("article_bogo_check", "check article with bogofilter (purely informational)");
1381 conRegFunc!(() {
1382 if (mainPane !is null && mainPane.lastDecodedTid) {
1383 conwriteln("relinking ", mainPane.lastDecodedTid, " (", chiroGetTagName(mainPane.lastDecodedTid).getData, ")...");
1384 chiroSupportRelinkTagThreads(mainPane.lastDecodedTid);
1385 mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
1386 postScreenRebuild();
1388 })("folder_rebuild_index", "rebuild thread index");
1390 conRegFunc!(() {
1391 if (vbwin && !vbwin.closed && mainPane !is null /*&& mainPane.lastDecodedTid*/) {
1392 vbwin.postEvent(new RecalcAllTwitsEvent());
1394 })("folder_rebuild_twits", "rebuild all twits");
1396 conRegFunc!(() {
1397 if (mainPane !is null && mainPane.folderCurrTag.length) {
1398 auto w = new TagOptionsWindow(mainPane.folderCurrTag.getData);
1399 w.onUpdated = delegate void (tagname) {
1400 if (vbwin && !vbwin.closed && mainPane !is null && mainPane.lastDecodedTagName == tagname) {
1401 mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
1404 postScreenRebuild();
1406 })("folder_options", "show options window");
1409 conRegFunc!(() {
1410 if (auto fld = getActiveFolder) {
1411 fld.withBase(delegate (abase) {
1412 uint idx = fld.curidx;
1413 if (idx >= fld.length) {
1414 idx = 0;
1415 } else if (auto art = abase[fld.baseidx(idx)]) {
1416 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
1417 idx = (idx+1)%fld.length;
1420 foreach (immutable _; 0..fld.length) {
1421 auto art = abase[fld.baseidx(idx)];
1422 if (art !is null) {
1423 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
1424 fld.curidx = cast(int)idx;
1425 postScreenRebuild();
1426 return;
1429 idx = (idx+1)%fld.length;
1433 })("find_mine", "find mine article");
1436 // //////////////////////////////////////////////////////////////////// //
1437 conRegVar!dbg_dump_keynames("dbg_dump_key_names", "dump key names");
1440 // //////////////////////////////////////////////////////////////////// //
1441 conRegFunc!((uint idx) {
1442 if (vbwin is null || vbwin.closed) return;
1443 if (mainPane is null) return;
1444 if (mainPane.msglistCurrUId == 0) return;
1446 static auto statAttaches = LazyStatement!"View"(`
1447 SELECT
1448 idx AS idx
1449 , mime AS mime
1450 , name AS name
1451 , ChiroUnpack(content) AS content
1452 FROM attaches
1453 WHERE uid=:uid
1454 ORDER BY idx
1455 ;`);
1456 char[128] buf = void;
1457 uint attidx = 0;
1458 foreach (auto row; statAttaches.st.bind(":uid", mainPane.msglistCurrUId).range) {
1459 import core.stdc.stdio : snprintf;
1460 if (idx != 0) { --idx; ++attidx; continue; }
1461 if (!row.mime!SQ3Text.startsWith("image")) return;
1462 auto name = row.name!SQ3Text.getExtension;
1463 if (name.length == 0) return;
1464 try {
1465 DynStr fname;
1466 fname = "/tmp/_viewimage_";
1468 import std.uuid;
1469 UUID id = randomUUID();
1470 foreach (immutable ubyte b; id.data[]) {
1471 fname ~= "0123456789abcdef"[b>>4];
1472 fname ~= "0123456789abcdef"[b&0x0f];
1474 fname ~= name;
1476 conwriteln("writing attach #", attidx, " to '", fname.getData, "'");
1477 VFile fo = VFile(fname.getData, "w");
1478 fo.rawWriteExact(row.content!SQ3Text);
1479 fo.close();
1481 auto vtid = spawn(&imageViewThread, thisTid);
1482 string fnamestr = fname.getData.idup;
1483 vtid.send(ImgViewCommand(fnamestr));
1484 } catch (Exception e) {
1485 conwriteln("ERROR writing attachment: ", e.msg);
1487 break;
1489 })("attach_view", "view attached image: attach_view index");
1492 // //////////////////////////////////////////////////////////////////// //
1493 conRegFunc!((uint idx, ConString userfname=null) {
1494 if (vbwin is null || vbwin.closed) return;
1495 if (mainPane is null) return;
1496 if (mainPane.msglistCurrUId == 0) return;
1498 static auto statAttaches = LazyStatement!"View"(`
1499 SELECT
1500 idx AS idx
1501 , mime AS mime
1502 , name AS name
1503 , ChiroUnpack(content) AS content
1504 FROM attaches
1505 WHERE uid=:uid
1506 ORDER BY idx
1507 ;`);
1508 char[128] buf = void;
1509 uint attidx = 0;
1510 foreach (auto row; statAttaches.st.bind(":uid", mainPane.msglistCurrUId).range) {
1511 import core.stdc.stdio : snprintf;
1512 if (idx != 0) { --idx; ++attidx; continue; }
1513 try {
1514 auto name = row.name!SQ3Text/*.decodeSubj*/;
1515 DynStr fname;
1517 if (userfname.length) {
1518 fname = userfname;
1519 } else {
1520 fname = "/tmp/";
1521 name = name.sanitizeFileNameStr;
1522 if (name.length == 0) {
1523 auto len = snprintf(buf.ptr, buf.sizeof, "attach_%02u.bin", attidx);
1524 fname ~= buf[0..cast(usize)len];
1525 } else {
1526 fname ~= name;
1530 conwriteln("writing attach #", attidx, " to '", fname.getData, "'");
1531 VFile fo = VFile(fname.getData, "w");
1532 fo.rawWriteExact(row.content!SQ3Text);
1533 fo.close();
1534 } catch (Exception e) {
1535 conwriteln("ERROR writing attachment: ", e.msg);
1537 break;
1539 })("attach_save", "save attach: attach_save index [filename]");
1543 // ////////////////////////////////////////////////////////////////////////// //
1544 //FIXME: turn into property
1545 final class ArticleTextScrollEvent {}
1546 __gshared ArticleTextScrollEvent evArticleScroll;
1547 shared static this () {
1548 evArticleScroll = new ArticleTextScrollEvent();
1551 void postArticleScroll () {
1552 if (vbwin !is null && !vbwin.eventQueued!ArticleTextScrollEvent) vbwin.postTimeout(evArticleScroll, 25);
1556 // ////////////////////////////////////////////////////////////////////////// //
1557 class MarkAsUnreadEvent {
1558 uint tagid;
1559 uint uid;
1562 __gshared int unreadTimeoutInterval = 600;
1563 __gshared int scrollKeepLines = 3;
1566 void postMarkAsUnreadEvent () {
1567 if (unreadTimeoutInterval > 0 && unreadTimeoutInterval < 10000 && vbwin !is null && mainPane !is null) {
1568 if (!mainPane.msglistCurrUId) return;
1569 if (chiroGetMessageUnread(mainPane.lastDecodedTid, mainPane.msglistCurrUId)) {
1570 //conwriteln("setting new unread timer");
1571 auto evt = new MarkAsUnreadEvent();
1572 evt.tagid = mainPane.lastDecodedTid;
1573 evt.uid = mainPane.msglistCurrUId;
1574 vbwin.postTimeout(evt, unreadTimeoutInterval);
1580 // ////////////////////////////////////////////////////////////////////////// //
1581 __gshared MainPaneWindow mainPane;
1584 final class MainPaneWindow : SubWindow {
1585 string[] emlines; // in utf
1586 uint emlinesBeforeAttaches;
1587 uint lastDecodedUId;
1588 uint lastDecodedTid; // for which folder we last build our view?
1589 uint lastMaxTidUid;
1590 DynStr lastDecodedTagName;
1591 DynStr arttoname;
1592 DynStr arttomail;
1593 DynStr artfromname;
1594 DynStr artfrommail;
1595 DynStr artsubj;
1596 DynStr arttime;
1597 int articleTextTopLine = 0;
1598 int articleDestTextTopLine = 0;
1599 //__gshared Timer unreadTimer; // as main pane is never destroyed, there's no need to kill the timer
1600 //__gshared Folder unreadFolder;
1601 //__gshared uint unreadIdx;
1602 int linesInHeader;
1603 // folder view
1604 DynStr folderTopTag = null;
1605 DynStr folderCurrTag = null;
1606 int folderTopIndex = 0;
1607 int folderCurrIndex = 0;
1608 // message list view
1609 uint msglistTopUId = 0;
1610 uint msglistCurrUId = 0;
1611 bool saveMsgListPositions = false;
1612 MonoTime lastStateSaveTime;
1614 this () {
1615 super(null, 0, 0, screenWidth, screenHeight);
1616 mType = Type.OnBottom;
1617 add();
1618 loadSavedState();
1619 lastStateSaveTime = MonoTime.currTime;
1622 void updateViewsIfTid (uint tid) {
1623 if (tid && lastDecodedTid == tid) {
1624 switchToFolderTid(lastDecodedTid, forced:true);
1628 void checkSaveState () {
1629 if (lastStateSaveTime+dur!"seconds"(30) <= MonoTime.currTime) {
1630 //writeln("saving state...");
1631 saveCurrentState();
1632 lastStateSaveTime = MonoTime.currTime;
1633 if (lastDecodedTid) {
1634 immutable maxuid = chiroGetTreePaneTableMaxUId();
1635 if (maxuid && maxuid > lastMaxTidUid) {
1636 // something was changed in the database, rescal
1637 updateViewsIfTid(lastDecodedTid);
1643 void saveCurrentState () {
1644 transacted!"Conf"{
1645 chiroSetOption("/mainpane/folders/toptid", folderTopTag);
1646 chiroSetOption("/mainpane/folders/currtid", folderCurrTag);
1647 saveThreadListPositions(transaction:false);
1651 void loadSavedState () {
1652 chiroGetOption(folderTopTag, "/mainpane/folders/toptid");
1653 chiroGetOption(folderCurrTag, "/mainpane/folders/currtid");
1654 //conwriteln("curr: ", folderCurrTag);
1655 //conwriteln(" top: ", folderTopTag);
1656 rescanFolders(forced:true);
1660 private void threadListPositionDirty () nothrow @safe @nogc {
1661 saveMsgListPositions = true;
1664 private void saveThreadListPositionsInternal () {
1665 if (lastDecodedTagName.length == 0) return;
1666 import core.stdc.stdio : snprintf;
1667 const(char)[] tn = lastDecodedTagName.getData;
1668 char[128] xname = void;
1669 auto xlen = snprintf(xname.ptr, xname.sizeof, "/mainpane/msgview/current%.*s", cast(uint)tn.length, tn.ptr);
1670 chiroSetOptionUInts(xname[0..xlen], msglistTopUId, msglistCurrUId);
1673 private void saveThreadListPositions (immutable bool transaction=true) {
1674 if (!saveMsgListPositions) return;
1675 saveMsgListPositions = false;
1676 if (lastDecodedTid == 0) return;
1677 if (transaction) {
1678 transacted!"Conf"(&saveThreadListPositionsInternal);
1679 } else {
1680 saveThreadListPositionsInternal();
1684 private void loadThreadListPositions () {
1685 saveMsgListPositions = false;
1686 msglistTopUId = msglistCurrUId = 0;
1687 if (lastDecodedTid == 0) return;
1688 import core.stdc.stdio : snprintf;
1689 char[128] xname = void;
1690 const(char)[] tn = lastDecodedTagName.getData;
1691 auto xlen = snprintf(xname.ptr, xname.sizeof, "/mainpane/msgview/current%.*s", cast(uint)tn.length, tn.ptr);
1692 chiroGetOptionUInts(ref msglistTopUId, ref msglistCurrUId, xname[0..xlen]);
1693 setupUnreadTimer();
1697 private int getFolderMonthLimit () {
1698 import core.stdc.stdio : snprintf;
1699 char[1024] xname = void;
1700 if (lastDecodedTid == 0) return chiroGetOption!int("/mainpane/msgview/monthlimit", 6);
1701 bool exists;
1702 const(char)[] tn = lastDecodedTagName.getData;
1703 for (;;) {
1704 auto len = snprintf(xname.ptr, xname.sizeof, "/mainpane/msgview/monthlimit%.*s", cast(uint)tn.length, tn.ptr);
1705 int v = chiroGetOptionEx!int(xname[0..len], out exists);
1706 //writeln(tn, " :: ", v, " :: ", exists);
1707 if (exists) return v;
1708 auto slp = tn.lastIndexOf('/', 1);
1709 if (slp <= 0) break;
1710 tn = tn[0..slp];
1712 return chiroGetOption!int("/mainpane/msgview/monthlimit", 6);
1716 void rescanFolders (bool forced=false) {
1717 if (!.rescanFolders()) { if (!forced) return; }
1718 folderTopIndex = folderCurrIndex = 0;
1719 bool foundTop = false, foundCurr = false;
1720 foreach (immutable idx, const FolderInfo fi; folderList) {
1721 if (!foundTop && fi.name == folderTopTag) { foundTop = true; folderTopIndex = cast(int)idx; }
1722 if (!foundCurr && fi.name == folderCurrTag) { foundCurr = true; folderCurrIndex = cast(int)idx; }
1724 if (!foundTop) folderTopTag.clear();
1725 if (!foundCurr) folderCurrTag.clear();
1728 void folderMakeCurVisible () {
1729 rescanFolders();
1730 if (folderList.length) {
1731 if (folderCurrIndex >= folderList.length) folderCurrIndex = cast(uint)folderList.length-1;
1732 if (folderTopIndex >= folderList.length) folderTopIndex = cast(uint)folderList.length-1;
1733 if (folderCurrIndex-3 < folderTopIndex) {
1734 folderTopIndex = folderCurrIndex-3;
1735 if (folderTopIndex < 0) folderTopIndex = 0;
1737 int hgt = screenHeight/(gxTextHeightUtf+2)-1;
1738 if (hgt < 1) hgt = 1;
1739 if (folderCurrIndex+2 > folderTopIndex+hgt) {
1740 folderTopIndex = (folderCurrIndex+2 > hgt ? folderCurrIndex+2-hgt : 0);
1741 if (folderTopIndex < 0) folderTopIndex = 0;
1743 if (folderTopTag != folderList[folderTopIndex].name) folderTopTag = folderList[folderTopIndex].name;
1744 if (folderCurrTag != folderList[folderCurrIndex].name) folderCurrTag = folderList[folderCurrIndex].name;
1745 } else {
1746 folderCurrIndex = folderTopIndex = 0;
1750 private void switchToFolderTid (const uint tid, bool forced=false) {
1751 //setupTrayAnimation();
1752 if (!forced && tid == lastDecodedTid) return;
1753 saveThreadListPositions();
1754 if (lastDecodedTid != 0) chiroDeletePurgedWithTag(lastDecodedTid);
1755 // rescan
1756 lastDecodedTid = tid;
1757 if (tid != 0) {
1758 lastDecodedTagName = chiroGetTagName(tid);
1759 immutable int monthlimit = getFolderMonthLimit();
1760 chiroCreateTreePaneTable(tid, lastmonthes:monthlimit);
1761 // load position
1762 loadThreadListPositions();
1763 // top
1764 if (!chiroIsTreePaneTableUidValid(msglistTopUId)) {
1765 msglistTopUId = chiroGetTreePaneTableIndex2Uid(0);
1766 threadListPositionDirty();
1768 // current
1769 if (!chiroIsTreePaneTableUidValid(msglistCurrUId)) {
1770 msglistCurrUId = chiroGetTreePaneTableIndex2Uid(0);
1771 threadListPositionDirty();
1773 lastMaxTidUid = chiroGetTreePaneTableMaxUId();
1774 setupUnreadTimer();
1775 } else {
1776 lastDecodedTagName.clear();
1777 msglistTopUId = 0;
1778 msglistCurrUId = 0;
1779 chiroClearTreePaneTable();
1783 void folderSetToIndex (int idx) {
1784 if (idx < 0 || idx >= folderList.length) return;
1785 if (idx == folderCurrIndex) return;
1786 folderCurrIndex = idx;
1787 folderCurrTag = folderList[folderCurrIndex].name;
1788 switchToFolderTid(folderList[folderCurrIndex].tagid);
1791 bool folderUpOne () {
1792 if (folderList.length == 0) return false;
1793 if (folderCurrIndex <= 0) return false;
1794 --folderCurrIndex;
1795 folderCurrTag = folderList[folderCurrIndex].name;
1796 setupUnreadTimer();
1797 return true;
1800 bool folderDownOne () {
1801 if (folderList.length == 0) return false;
1802 if (folderCurrIndex+1 >= cast(int)folderList.length) return false;
1803 ++folderCurrIndex;
1804 folderCurrTag = folderList[folderCurrIndex].name;
1805 setupUnreadTimer();
1806 return true;
1809 bool threadListHome () {
1810 if (lastDecodedTid == 0) return false;
1811 immutable uint firstUid = chiroGetTreePaneTableFirstUid();
1812 if (!firstUid || firstUid == msglistCurrUId) return false;
1813 msglistCurrUId = firstUid;
1814 threadListPositionDirty();
1815 setupUnreadTimer();
1816 return true;
1819 bool threadListEnd () {
1820 if (lastDecodedTid == 0) return false;
1821 immutable uint lastUid = chiroGetTreePaneTableLastUid();
1822 if (!lastUid || lastUid == msglistCurrUId) return false;
1823 msglistCurrUId = lastUid;
1824 threadListPositionDirty();
1825 setupUnreadTimer();
1826 return true;
1829 bool threadListUp () {
1830 import iv.timer;
1831 if (lastDecodedTid == 0) return false;
1832 auto ctm = Timer(true);
1833 immutable uint prevUid = chiroGetTreePaneTablePrevUid(msglistCurrUId);
1834 ctm.stop;
1835 if (ChiroTimerExEnabled) writeln("threadListUp time: ", ctm);
1836 if (!prevUid || prevUid == msglistCurrUId) return false;
1837 msglistCurrUId = prevUid;
1838 threadListPositionDirty();
1839 setupUnreadTimer();
1840 return true;
1843 bool threadListDown () {
1844 import iv.timer;
1845 if (lastDecodedTid == 0) return false;
1846 auto ctm = Timer(true);
1847 immutable uint nextUid = chiroGetTreePaneTableNextUid(msglistCurrUId);
1848 ctm.stop;
1849 if (ChiroTimerExEnabled) writeln("threadListDown time: ", ctm);
1850 if (!nextUid || nextUid == msglistCurrUId) return false;
1851 msglistCurrUId = nextUid;
1852 threadListPositionDirty();
1853 setupUnreadTimer();
1854 return true;
1857 bool threadListScrollUp (bool movecurrent) {
1858 import iv.timer;
1859 if (lastDecodedTid == 0) return false;
1860 auto ctm = Timer(true);
1861 immutable uint topPrevUid = chiroGetTreePaneTablePrevUid(msglistTopUId);
1862 ctm.stop;
1863 if (ChiroTimerExEnabled) conwriteln("threadListScrollUp: prevtop time: ", ctm);
1864 if (!topPrevUid || topPrevUid == msglistTopUId) return false;
1865 ctm.restart;
1866 immutable uint currPrevUid = (movecurrent ? chiroGetTreePaneTablePrevUid(msglistCurrUId) : 0);
1867 ctm.stop;
1868 if (movecurrent && ChiroTimerExEnabled) conwriteln("threadListScrollUp: prevcurr time: ", ctm);
1869 if (movecurrent && !currPrevUid) return false;
1870 msglistTopUId = topPrevUid;
1871 if (movecurrent) {
1872 msglistCurrUId = currPrevUid;
1873 setupUnreadTimer();
1875 threadListPositionDirty();
1876 return true;
1879 bool threadListScrollDown (bool movecurrent) {
1880 import iv.timer;
1881 if (lastDecodedTid == 0) return false;
1882 auto ctm = Timer(true);
1883 immutable uint currNextUid = (movecurrent ? chiroGetTreePaneTableNextUid(msglistCurrUId) : 0);
1884 ctm.stop;
1885 if (movecurrent && ChiroTimerExEnabled) writeln("threadListScrollDown: nextcurr time: ", ctm);
1886 if (movecurrent && (!currNextUid || currNextUid == msglistCurrUId)) return false;
1887 ctm.restart;
1888 immutable uint topNextUid = chiroGetTreePaneTableNextUid(msglistTopUId);
1889 ctm.stop;
1890 if (ChiroTimerExEnabled) writeln("threadListScrollDown: nexttop time: ", ctm);
1891 if (!topNextUid) return false;
1892 msglistTopUId = topNextUid;
1893 if (movecurrent) {
1894 msglistCurrUId = currNextUid;
1895 setupUnreadTimer();
1897 threadListPositionDirty();
1898 return true;
1901 bool threadListPageUp () {
1902 import iv.timer;
1903 bool res = false;
1904 int hgt = guiThreadListHeight/gxTextHeightUtf-1;
1905 if (hgt < 1) hgt = 1;
1906 auto ctm = Timer(true);
1907 foreach (; 0..hgt) {
1908 if (!threadListScrollUp(movecurrent:true)) break;
1909 res = true;
1911 ctm.stop;
1912 if (ChiroTimerExEnabled) writeln("threadListPageUp time: ", ctm);
1913 if (res) setupUnreadTimer();
1914 return res;
1917 bool threadListPageDown () {
1918 import iv.timer;
1919 bool res = false;
1920 int hgt = guiThreadListHeight/gxTextHeightUtf-1;
1921 if (hgt < 1) hgt = 1;
1922 auto ctm = Timer(true);
1923 foreach (; 0..hgt) {
1924 if (!threadListScrollDown(movecurrent:true)) break;
1925 res = true;
1927 ctm.stop;
1928 if (ChiroTimerExEnabled) writeln("threadListPageDown time: ", ctm);
1929 if (res) setupUnreadTimer();
1930 return res;
1934 // //////////////////////////////////////////////////////////////////// //
1935 static struct WebLink {
1936 int ly; // in lines
1937 int x, len; // in pixels
1938 string url;
1939 string text; // visual text
1940 int attachnum = -1;
1941 bool nofirst = false;
1943 @property bool isAttach () const pure nothrow @safe @nogc { return (attachnum >= 0); }
1946 WebLink[] emurls;
1947 int lastUrlIndex = -1;
1949 void clearDecodedText () {
1950 emurls[] = WebLink.init;
1951 emlines[] = null;
1952 emurls.length = 0;
1953 emurls.assumeSafeAppend;
1954 emlines.length = 0;
1955 emlines.assumeSafeAppend;
1956 lastDecodedUId = 0;
1957 articleTextTopLine = 0;
1958 articleDestTextTopLine = 0;
1959 lastUrlIndex = -1;
1960 arttoname.clear();
1961 arttomail.clear();
1962 artfromname.clear();
1963 artfrommail.clear();
1964 artsubj.clear();
1965 arttime.clear();
1968 // <0: not on url
1969 int findUrlIndexAt (int mx, int my) {
1970 int tpX0 = guiGroupListWidth+2+1;
1971 int tpX1 = screenWidth-1-guiMessageTextLPad*2-guiScrollbarWidth;
1972 int tpY0 = guiThreadListHeight+1;
1973 int tpY1 = screenHeight-1;
1975 int y = tpY0+linesInHeader*gxTextHeightUtf+2+1+guiMessageTextVPad;
1977 if (mx < tpX0 || mx > tpX1) return -1;
1978 if (my < y || my > tpY1) return -1;
1980 mx -= tpX0;
1982 // yeah, i can easily calculate this, i know
1983 uint idx = articleTextTopLine;
1984 while (idx < emlines.length && y < screenHeight) {
1985 if (my >= y && my < y+gxTextHeightUtf) {
1986 foreach (immutable uidx, const ref WebLink wl; emurls) {
1987 //conwriteln("checking url [#", uidx, "]; idx=", idx, "; ly=", wl.ly);
1988 if (wl.ly == idx) {
1989 if (mx >= wl.x && mx < wl.x+wl.len) return cast(int)uidx;
1993 ++idx;
1994 y += gxTextHeightUtf+guiMessageTextInterline;
1997 return -1;
2000 WebLink* findUrlAt (int mx, int my) {
2001 auto uidx = findUrlIndexAt(mx, my);
2002 return (uidx >= 0 ? &emurls[uidx] : null);
2005 private void emlDetectUrls (uint textlines) {
2006 static immutable string[3] protos = [ "https", "http", "ftp" ];
2008 lastUrlIndex = -1;
2010 static sptrdiff urlepos (const(char)[] s, sptrdiff spos) {
2011 assert(spos < s.length);
2012 spos = s.indexOf("://", spos);
2013 assert(spos >= 0);
2014 spos += 3;
2015 // host
2016 while (spos < s.length) {
2017 char ch = s[spos];
2018 if (ch == '/') break;
2019 if (ch <= ' ') return spos;
2020 if (ch != '.' && ch != '-' && !ch.isalnum) return spos;
2021 ++spos;
2023 if (spos >= s.length) return cast(sptrdiff)s.length;
2024 // path
2025 assert(s[spos] == '/');
2026 char[16] brcmap;
2027 usize brlevel = 0;
2028 bool wasSharp = false;
2029 for (; spos < s.length; ++spos) {
2030 import iv.strex : isalnum;
2031 char ch = s[spos];
2032 if (ch <= ' ' || ch == '<' || ch == '>' || ch == '"' || ch == '\'' || ch >= 127) return spos;
2033 // hash
2034 if (ch == '#') {
2035 if (wasSharp) return spos;
2036 wasSharp = true;
2037 brlevel = 0;
2038 continue;
2040 // path delimiter
2041 if (ch == '/') {
2042 brlevel = 0;
2043 continue;
2045 // opening bracket
2046 if (ch == '(' || ch == '{' || ch == '[') {
2047 if (brlevel >= brcmap.length) return spos; // too nested
2048 if (s.length-spos < 2) return spos; // no more chars, ignore
2049 if (!isalnum(s[spos+1]) && s[spos+1] != '_') return spos; // ignore
2050 // looks like URL part
2051 final switch (ch) {
2052 case '(': ch = ')'; break;
2053 case '[': ch = ']'; break;
2054 case '{': ch = '}'; break;
2056 brcmap[brlevel++] = ch;
2057 continue;
2059 // closing bracket
2060 if (ch == ')' || ch == '}' || ch == ']') {
2061 if (brlevel == 0 || ch != brcmap[brlevel-1]) return spos;
2062 --brlevel;
2063 continue;
2065 // other punctuation
2066 if (brlevel == 0 && !isalnum(ch)) {
2067 // other special chars
2068 if (s.length-spos < 2) return spos; // no more chars, ignore
2069 if (!isalnum(s[spos+1]) && s[spos+1] != '_') {
2070 if (ch == '.' || ch == '!' || ch == ';' || ch == ',' || s[spos+1] != ch) return spos; // ignore
2073 ++spos;
2075 if (spos >= s.length) spos = cast(sptrdiff)s.length;
2076 return spos;
2079 if (textlines > emlines.length) textlines = cast(uint)emlines.length; // just in case
2080 foreach (immutable cy, string s; emlines[0..textlines]) {
2081 if (s.length == 0) continue;
2082 auto pos = s.indexOf("://");
2083 while (pos > 0) {
2084 bool found = false;
2085 auto spos = pos;
2086 foreach (string proto; protos) {
2087 if (spos >= proto.length && proto.strEquCI(s[spos-proto.length..spos])) {
2088 if (spos == proto.length || !s[spos-proto.length-1].isalnum) {
2089 found = true;
2090 spos -= proto.length;
2091 break;
2095 if (found) {
2096 // find URL end
2097 auto epos = urlepos(s, spos);
2098 WebLink wl;
2099 wl.nofirst = (spos > 0);
2100 wl.ly = cast(int)cy;
2101 auto kr = GxKerning(4);
2102 s[0..epos].utfByDCharSPos(delegate (dchar ch, usize stpos) {
2103 int w = kr.fixWidthPre(ch);
2104 if (stpos == spos) wl.x = w;
2106 wl.len = kr.finalWidth-wl.x;
2107 wl.url = wl.text = s[spos..epos];
2108 emurls ~= wl;
2109 pos = epos;
2110 } else {
2111 ++pos;
2113 pos = s.indexOf("://", pos);
2117 int attachcount = 0;
2118 foreach (immutable uint cy; textlines..cast(uint)emlines.length) {
2119 string s = emlines[cy];
2120 if (s.length == 0) continue;
2121 auto spos = s.indexOf("attach:");
2122 if (spos < 0) continue;
2123 auto epos = spos+7;
2124 while (epos < s.length && s.ptr[epos] > ' ') ++epos;
2125 //if (attachcount >= parts.length) break;
2126 WebLink wl;
2127 wl.nofirst = (spos > 0);
2128 wl.ly = cast(int)cy;
2129 //wl.x = gxTextWidthUtf(s[0..spos]);
2130 //wl.len = gxTextWidthUtf(s[spos..epos]);
2131 auto kr = GxKerning(4);
2132 s[0..epos].utfByDCharSPos(delegate (dchar ch, usize stpos) {
2133 int w = kr.fixWidthPre(ch);
2134 if (stpos == spos) wl.x = w;
2136 wl.len = kr.finalWidth-wl.x;
2137 wl.url = wl.text = s[spos..epos];
2138 //if (spos > 0) ++wl.x; // this makes text bolder, no need to
2139 wl.attachnum = attachcount;
2140 //wl.attachfname = s[spos+7..epos];
2141 //wl.part = parts[attachcount];
2142 ++attachcount;
2143 emurls ~= wl;
2147 bool needToDecodeArticleTextNL (uint uid) const nothrow @trusted @nogc {
2148 return (lastDecodedUId != uid);
2151 // fld is locked here
2152 void decodeArticleTextNL (uint uid) {
2153 static auto stmtGet = LazyStatement!"View"(`
2154 SELECT
2155 from_name AS fromName
2156 , from_mail AS fromMail
2157 , to_name AS toName
2158 , to_mail AS toMail
2159 , subj AS subj
2160 , datetime(threads.time, 'unixepoch') AS time
2161 , ChiroUnpack(content_text.content) AS text
2162 , ChiroUnpack(content_html.content) AS html
2163 FROM info
2164 INNER JOIN threads USING(uid)
2165 INNER JOIN content_text USING(uid)
2166 INNER JOIN content_html USING(uid)
2167 WHERE uid=:uid
2168 LIMIT 1
2169 ;`);
2171 if (!needToDecodeArticleTextNL(uid)) return;
2173 //if (uid == 0) { clearDecodedText(); return; }
2174 clearDecodedText();
2175 lastDecodedUId = uid;
2177 // get article content
2178 //FIXME: see `art.getTextContent()` for GPG decryption!
2179 DynStr artcontent;
2180 bool ishtml = false;
2181 bool htmlheader = false;
2182 foreach (auto row; stmtGet.st.bind(":uid", uid).range) {
2183 auto text = row.text!SQ3Text;
2184 auto html = row.html!SQ3Text;
2185 if (text.xstrip.length == 0 && html.xstrip.length) {
2186 version(article_can_into_html) {
2187 try {
2188 string s = htmlToText(html.idup, false);
2189 artcontent ~= s;
2190 artcontent.removeASCIICtrls();
2191 htmlheader = true;
2192 } catch (Exception e) {
2193 artcontent ~= html;
2195 } else {
2196 artcontent ~= html;
2198 ishtml = true;
2199 } else if (text.length != 0) {
2200 artcontent ~= text;
2201 } else {
2202 artcontent ~= "no text content";
2204 arttoname = row.toName!SQ3Text;
2205 arttomail = row.toMail!SQ3Text;
2206 artfromname = row.fromName!SQ3Text;
2207 artfrommail = row.fromMail!SQ3Text;
2208 artsubj = row.subj!SQ3Text;
2209 arttime = row.time!SQ3Text;
2210 if (artsubj.length == 0) artsubj = "no subject";
2213 if (artcontent.length == 0) return; // no text
2214 emlines ~= null; // hack; this dummy line will be removed
2215 bool skipEmptyLines = true;
2217 if (htmlheader) {
2218 emlines ~= "\x01==============================================";
2219 emlines ~= "\x01--- HTML CONTENT ---";
2220 emlines ~= "\x01==============================================";
2221 //emlines ~= null;
2224 bool lastEndsWithSpace () { return (emlines[$-1].length ? emlines[$-1][$-1] == ' ' : false); }
2226 int lastQLevel = 0;
2228 static string addQuotes(T:const(char)[]) (T s, int qlevel) {
2229 enum QuoteStr = ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ";
2230 if (qlevel <= 0) {
2231 static if (is(T == string) || is (T == typeof(null))) return s; else return s.idup;
2233 if (qlevel > QuoteStr.length-1) qlevel = cast(int)QuoteStr.length-1;
2234 return QuoteStr[$-qlevel-1..$]~s;
2237 // returns quote level
2238 static int removeQuoting (ref ConString s) {
2239 // calculate quote level
2240 int qlevel = 0;
2241 if (s.length && s[0] == '>') {
2242 usize lastqpos = 0, pos = 0;
2243 while (pos < s.length) {
2244 if (s.ptr[pos] != '>') {
2245 if (s.ptr[pos] != ' ') break;
2246 } else {
2247 lastqpos = pos;
2248 ++qlevel;
2250 ++pos;
2252 if (s.length-lastqpos > 1 && s.ptr[lastqpos+1] == ' ') ++lastqpos;
2253 ++lastqpos;
2254 s = s[lastqpos..$];
2256 return qlevel;
2259 bool inCode = false;
2261 void putLine (ConString s) {
2262 int qlevel = (ishtml ? 0 : removeQuoting(s));
2263 //conwriteln("qlevel=", qlevel, "; s=[", s, "]");
2264 // empty line: just insert it
2265 if (s.length == 0) {
2266 if (skipEmptyLines) return;
2267 if (qlevel == 0 && emlines[$-1].xstripright.length == 0) return;
2268 emlines ~= addQuotes(null, qlevel).xstripright;
2269 } else {
2270 if (s.xstrip.length == 0) {
2271 if (skipEmptyLines) return;
2272 if (qlevel == 0 && emlines[$-1].xstripright.length == 0) return;
2274 skipEmptyLines = false;
2275 // join code lines if it is possible
2276 if (inCode && qlevel == lastQLevel && lastEndsWithSpace) {
2277 //conwriteln("<", s, ">");
2278 emlines[$-1] ~= addQuotes(s.xstrip, qlevel);
2279 return;
2281 // two spaces at the beginning usually means "this is code"; don't wrap it
2282 if (s.length >= 1 && s[0] == '\t') {
2283 emlines ~= addQuotes(s, qlevel);
2284 // join next lines if it is possible
2285 inCode = true;
2286 //conwriteln("[", s, "]");
2287 lastQLevel = qlevel;
2288 return;
2290 inCode = false;
2291 // can we append?
2292 bool newline = false;
2293 if (lastQLevel != qlevel || !lastEndsWithSpace) {
2294 newline = true;
2295 } else {
2296 // append words
2297 //while (s.length > 1 && s.ptr[0] <= ' ') s = s[1..$];
2299 while (s.length) {
2300 usize epos = 0;
2301 if (newline) {
2302 while (epos < s.length && s.ptr[epos] <= ' ') ++epos;
2303 } else {
2304 //assert(s[0] > ' ');
2305 while (epos < s.length && s.ptr[epos] <= ' ') ++epos;
2307 while (epos < s.length && s.ptr[epos] > ' ') ++epos;
2308 auto xlen = epos;
2309 while (epos < s.length && s.ptr[epos] <= ' ') ++epos;
2310 if (!newline && emlines[$-1].length+xlen <= 80) {
2311 // no wrapping, continue last line
2312 emlines[$-1] ~= s[0..epos];
2313 } else {
2314 newline = false;
2315 // wrapping; new line
2316 emlines ~= addQuotes(s[0..epos], qlevel);
2318 s = s[epos..$];
2320 if (newline) emlines ~= addQuotes(null, qlevel);
2322 lastQLevel = qlevel;
2325 try {
2326 //foreach (ConString s; LineIterator!false(art.getTextContent)) putLine(s);
2327 const(char)[] buf = artcontent;
2328 while (buf.length) {
2329 usize epos = skipOneLine(buf, 0);
2330 usize eend = epos;
2331 if (eend >= 2 && buf[eend-2] == '\r' && buf[eend-1] == '\n') eend -= 2;
2332 else if (eend >= 1 && buf[eend-1] == '\n') eend -= 1;
2333 putLine(buf[0..eend]);
2334 buf = buf[epos..$];
2336 // remove first dummy line
2337 if (emlines.length) emlines = emlines[1..$];
2338 // remove trailing empty lines
2339 while (emlines.length && emlines[$-1].xstrip.length == 0) emlines.length -= 1;
2340 } catch (Exception e) {
2341 conwriteln("================================= ERROR: ", e.msg, " =================================");
2342 conwriteln(e.toString);
2346 // attaches
2347 auto lcount = cast(uint)emlines.length;
2348 emlinesBeforeAttaches = lcount;
2350 uint attcount = 0;
2351 static auto statAttaches = LazyStatement!"View"(`
2352 SELECT
2353 idx AS idx
2354 , mime AS mime
2355 , name AS name
2356 FROM attaches
2357 WHERE uid=:uid
2358 ORDER BY idx
2359 ;`);
2360 foreach (auto row; statAttaches.st.bind(":uid", uid).range) {
2361 import core.stdc.stdio : snprintf;
2362 if (attcount == 0) { emlines ~= null; emlines ~= null; }
2363 DynStr att;
2364 char[128] buf = void;
2365 //if (type.length == 0) type = "unknown/unknown";
2366 //string s = " [%02s] attach:%s -- %s".format(attcount, Article.fixAttachmentName(filename), type);
2367 auto mime = row.mime!SQ3Text;
2368 auto name = row.name!SQ3Text/*.decodeSubj*/;
2369 auto len = snprintf(buf.ptr, buf.sizeof, " [%2u] attach:%.*s -- %.*s",
2370 attcount, cast(uint)name.length, name.ptr, cast(uint)mime.length, mime.ptr);
2371 assert(len > 0);
2372 if (len > buf.sizeof) len = cast(int)buf.sizeof;
2373 emlines ~= buf[0..len].idup;
2374 ++attcount;
2376 /*FIXME
2377 art.forEachAttachment(delegate(ConString type, ConString filename) {
2378 if (attcount == 0) { emlines ~= null; emlines ~= null; }
2379 import std.format : format;
2380 if (type.length == 0) type = "unknown/unknown";
2381 string s = " [%02s] attach:%s -- %s".format(attcount, Article.fixAttachmentName(filename), type);
2382 emlines ~= s;
2383 ++attcount;
2384 return false;
2387 emlDetectUrls(lcount);
2390 @property int visibleArticleLines () {
2391 int y = guiThreadListHeight+1+linesInHeader*gxTextHeightUtf+2+guiMessageTextVPad;
2392 return (screenHeight-y)/(gxTextHeightUtf+guiMessageTextInterline);
2395 void normalizeArticleTopLine () {
2396 int lines = visibleArticleLines;
2397 if (lines < 1 || emlines.length <= lines) {
2398 articleTextTopLine = 0;
2399 articleDestTextTopLine = 0;
2400 } else {
2401 if (articleTextTopLine < 0) articleTextTopLine = 0;
2402 if (articleTextTopLine+lines > emlines.length) {
2403 articleTextTopLine = cast(int)emlines.length-lines;
2404 if (articleTextTopLine < 0) articleTextTopLine = 0;
2409 void doScrollStep () {
2410 auto oldtop = articleTextTopLine;
2411 foreach (immutable _; 0..6) {
2412 normalizeArticleTopLine();
2413 if (articleDestTextTopLine < articleTextTopLine) {
2414 --articleTextTopLine;
2415 } else if (articleDestTextTopLine > articleTextTopLine) {
2416 ++articleTextTopLine;
2417 } else {
2418 break;
2420 normalizeArticleTopLine();
2422 if (articleTextTopLine == oldtop) {
2423 // can't scroll anymore
2424 articleDestTextTopLine = articleTextTopLine;
2425 return;
2427 postScreenRebuild();
2428 postArticleScroll();
2431 void scrollBy (int delta) {
2432 articleDestTextTopLine += delta;
2433 doScrollStep();
2436 void scrollByPageUp () {
2437 int lines = visibleArticleLines-scrollKeepLines;
2438 if (lines < 1) lines = 1;
2439 scrollBy(-lines);
2442 void scrollByPageDown () {
2443 int lines = visibleArticleLines-scrollKeepLines;
2444 if (lines < 1) lines = 1;
2445 scrollBy(lines);
2448 // ////////////////////////////////////////////////////////////////// //
2449 // this also fixes current/top uids
2450 void getAndFixThreadListIndicies (out int topidx, out int curridx) {
2451 int hgt = guiThreadListHeight/gxTextHeightUtf-1;
2452 if (hgt < 1) hgt = 1;
2454 topidx = chiroGetTreePaneTableUid2Index(msglistTopUId);
2455 immutable origTopIdx = topidx;
2456 if (topidx < 0) topidx = 0;
2457 curridx = chiroGetTreePaneTableUid2Index(msglistCurrUId);
2458 immutable origCurrIdx = curridx;
2459 if (curridx < 0) curridx = 0;
2461 //conwriteln("topuid: ", msglistTopUId, "; topidx=", topidx);
2462 //conwriteln("curruid: ", msglistCurrUId, "; curridx=", curridx);
2464 if (curridx-3 < topidx) {
2465 topidx = curridx-3;
2466 if (topidx < 0) topidx = 0;
2468 if (curridx+3 > topidx+hgt) {
2469 topidx = (curridx+3 > hgt ? curridx+3-hgt : 0);
2470 if (topidx < 0) topidx = 0;
2473 if (origCurrIdx != curridx || msglistCurrUId == 0) {
2474 immutable ocurr = msglistCurrUId;
2475 msglistCurrUId = chiroGetTreePaneTableIndex2Uid(curridx);
2476 if (ocurr != msglistCurrUId) {
2477 threadListPositionDirty();
2478 setupUnreadTimer();
2482 if (origTopIdx != topidx || msglistTopUId == 0) {
2483 immutable otop = msglistTopUId;
2484 msglistTopUId = chiroGetTreePaneTableIndex2Uid(topidx);
2485 if (otop != msglistTopUId) threadListPositionDirty();
2489 void setupUnreadTimer () {
2490 postMarkAsUnreadEvent();
2493 //enum gxRGBA = (clampToByte(a)<<24)|(clampToByte(r)<<16)|(clampToByte(g)<<8)|clampToByte(b);
2494 uint groupListBackColor = gxRGB!(30, 30, 30);
2495 uint groupListDivLineColor = gxRGB!(255, 255, 255);
2497 uint threadListBackColor = gxRGB!(30, 30, 30);
2498 uint threadListDivLineColor = gxRGB!(255, 255, 255);
2500 uint messageHeaderBackColor = gxRGB!(20, 20, 20);
2501 uint messageHeaderFromColor = gxRGB!(0, 128, 128);
2502 uint messageHeaderToColor = gxRGB!(0, 128, 128);
2503 uint messageHeaderSubjColor = gxRGB!(0, 128, 128);
2504 uint messageHeaderDateColor = gxRGB!(0, 128, 128);
2505 uint messageHeaderDivLineColor = gxRGB!(180, 180, 180);
2507 uint messageTextBackColor = gxRGB!(37, 37, 37);
2508 uint messageTextNormalColor = gxRGB!(128+46, 128+46, 128+46);
2509 uint messageTextQuote0Color = gxRGB!(128, 128, 0);
2510 uint messageTextQuote1Color = gxRGB!(0, 128, 128);
2511 uint messageTextHtmlHeaderColor = gxRGB!(128, 0, 128);
2512 uint messageTextLinkColor = gxRGB!(0, 200, 200);
2513 uint messageTextLinkHoverColor = gxRGB!(0, 255, 255);
2514 uint messageTextLinkPressedColor = gxRGB!(255, 0, 255);
2516 uint twithShadeColor = gxRGBA!(0, 0, 80, 127);
2517 uint twithTextColor = gxRGB!(255, 0, 0);
2518 uint twithTextOutlineColor = gxRGB!(0, 0, 0);
2520 // //////////////////////////////////////////////////////////////////// //
2521 //TODO: move parts to widgets
2522 override void onPaint () {
2523 gxClipReset();
2525 gxFillRect(0, 0, guiGroupListWidth, screenHeight, groupListBackColor);
2526 gxVLine(guiGroupListWidth, 0, screenHeight, groupListDivLineColor);
2528 gxFillRect(guiGroupListWidth+1, 0, screenWidth, guiThreadListHeight, threadListBackColor);
2529 gxHLine(guiGroupListWidth+1, guiThreadListHeight, screenWidth, threadListDivLineColor);
2531 // ////////////////////////////////////////////////////////////////// //
2532 void drawArticle (uint uid) {
2533 import core.stdc.stdio : snprintf;
2534 import std.format : format;
2535 import std.datetime;
2536 char[128] tbuf;
2537 const(char)[] tbufs;
2539 void xfmt (string s, const(char)[][] strs...) {
2540 int dpos = 0;
2541 void puts (const(char)[] s...) {
2542 foreach (char ch; s) {
2543 if (dpos >= tbuf.length) break;
2544 tbuf[dpos++] = ch;
2547 while (s.length) {
2548 if (strs.length && s.length > 1 && s[0] == '%' && s[1] == 's') {
2549 puts(strs[0]);
2550 strs = strs[1..$];
2551 s = s[2..$];
2552 } else {
2553 puts(s[0]);
2554 s = s[1..$];
2557 tbufs = tbuf[0..dpos];
2560 if (needToDecodeArticleTextNL(uid)) {
2561 decodeArticleTextNL(uid);
2565 gxClipX0 = guiGroupListWidth+2;
2566 gxClipX1 = screenWidth-1;
2567 gxClipY0 = guiThreadListHeight+1;
2568 gxClipY1 = screenHeight-1;
2570 gxClipRect = GxRect(GxPoint(guiGroupListWidth+2, guiThreadListHeight+1), GxPoint(screenWidth-1, screenHeight-1));
2572 int msx = lastMouseX;
2573 int msy = lastMouseY;
2575 int curDrawYMul = 1;
2577 // header
2578 immutable int hdrHeight = (3+(arttoname.length || arttomail.length ? 1 : 0))*gxTextHeightUtf+2;
2579 gxFillRect(gxClipRect.x0, gxClipRect.y0, gxClipRect.x1-gxClipRect.x0+1, hdrHeight, messageHeaderBackColor);
2581 if (artfromname.length) {
2582 xfmt("From: %s <%s>", artfromname, artfrommail);
2583 } else {
2584 xfmt("From: %s", artfrommail);
2586 gxDrawTextUtf(gxClipRect.x0+guiMessageTextLPad, gxClipRect.y0+0*gxTextHeightUtf+1, tbufs, messageHeaderFromColor);
2587 if (arttoname.length || arttomail.length) {
2588 if (arttoname.length) {
2589 xfmt("To: %s <%s>", arttoname, arttomail);
2590 } else {
2591 xfmt("To: %s", arttomail);
2593 gxDrawTextUtf(gxClipRect.x0+guiMessageTextLPad, gxClipRect.y0+curDrawYMul*gxTextHeightUtf+1, tbufs, messageHeaderToColor);
2594 ++curDrawYMul;
2596 xfmt("Subject: %s", (artsubj.length ? artsubj : "no subject"));
2597 gxDrawTextUtf(gxClipRect.x0+guiMessageTextLPad, gxClipRect.y0+curDrawYMul*gxTextHeightUtf+1, tbufs, messageHeaderSubjColor);
2598 ++curDrawYMul;
2600 //auto t = SysTime.fromUnixTime(arttime);
2601 //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);
2602 auto tlen = snprintf(tbuf.ptr, tbuf.length, "Date: %.*s", cast(uint)arttime.length, arttime.ptr);
2603 gxDrawTextUtf(gxClipRect.x0+guiMessageTextLPad, gxClipRect.y0+curDrawYMul*gxTextHeightUtf+1, tbuf[0..tlen], messageHeaderDateColor);
2604 ++curDrawYMul;
2608 // text
2609 linesInHeader = curDrawYMul;
2610 int y = gxClipRect.y0+curDrawYMul*gxTextHeightUtf+2;
2612 gxHLine(gxClipRect.x0, y, gxClipRect.x1-gxClipRect.x0+1, messageHeaderDivLineColor);
2613 ++y;
2614 gxFillRect(gxClipRect.x0, y, gxClipRect.x1-gxClipRect.x0+1, screenHeight-y, messageTextBackColor);
2615 y += guiMessageTextVPad;
2617 immutable sty = y;
2619 normalizeArticleTopLine();
2621 bool drawUpMark = (articleTextTopLine > 0);
2622 bool drawDownMark = false;
2624 uint idx = articleTextTopLine;
2625 bool msvisible = isMouseVisible;
2626 bool checkQuotes = true;
2627 if (emlines.length && emlines[0].length && emlines[0][0] == 1) {
2628 // html content
2629 checkQuotes = false;
2631 while (idx < emlines.length && y < screenHeight) {
2632 int qlevel = 0;
2633 string s = emlines[idx];
2635 if (checkQuotes) {
2636 foreach (immutable char ch; s) {
2637 if (ch <= ' ') continue;
2638 if (ch != '>') break;
2639 ++qlevel;
2643 uint clr = messageTextNormalColor;
2644 if (qlevel) {
2645 final switch (qlevel%2) {
2646 case 0: clr = messageTextQuote0Color; break;
2647 case 1: clr = messageTextQuote1Color; break;
2651 if (!checkQuotes && s.length && s[0] == 1) {
2652 clr = messageTextHtmlHeaderColor;
2653 s = s[1..$];
2656 gxDrawTextUtf(GxDrawTextOptions.TabColor(4, clr), gxClipRect.x0+guiMessageTextLPad, y, s);
2658 foreach (const ref WebLink wl; emurls) {
2659 if (wl.ly == idx) {
2660 uint lclr = messageTextLinkColor;
2661 if (msvisible && msy >= y && msy < y+gxTextHeightUtf &&
2662 msx >= gxClipRect.x0+1+guiMessageTextLPad+wl.x &&
2663 msx < gxClipRect.x0+1+guiMessageTextLPad+wl.x+wl.len)
2665 lclr = (lastMouseLeft ? messageTextLinkPressedColor : messageTextLinkHoverColor);
2667 gxDrawTextUtf(GxDrawTextOptions.TabColorFirstFull(4, lclr, wl.nofirst), gxClipRect.x0+guiMessageTextLPad+wl.x, y, wl.text);
2671 if (gxClipRect.y1-y < gxTextHeightUtf && emlines.length-idx > 0) drawDownMark = true;
2673 ++idx;
2674 y += gxTextHeightUtf+guiMessageTextInterline;
2677 //gxDrawTextOutScaledUtf(1, gxClipRect.x1-gxTextWidthUtf(triangleDownStr)-3, sty, triangleDownStr, (drawUpMark ? gxRGB!(255, 255, 255) : gxRGB!(85, 85, 85)), gxRGB!(0, 69, 69));
2678 //gxDrawTextOutScaledUtf(1, gxClipRect.x1-gxTextWidthUtf(triangleUpStr)-3, gxClipRect.y1-7, triangleUpStr, (drawDownMark ? gxRGB!(255, 255, 255) : gxRGB!(85, 85, 85)), gxRGB!(0, 69, 69));
2680 gxDrawScrollBar(GxRect(gxClipRect.x1-10, sty+10, 5, gxClipRect.y1-sty-17), cast(int)emlines.length-1, idx-1);
2682 bool twited = false;
2683 DynStr twittext = chiroGetMessageTwit(lastDecodedTid, uid, out twited);
2684 if (twited) {
2685 foreach (immutable dy; gxClipRect.y0+3*gxTextHeightUtf+2..gxClipRect.y1+1) {
2686 foreach (immutable dx; gxClipRect.x0..gxClipRect.x1+1) {
2687 if ((dx^dy)&1) gxPutPixel(dx, dy, twithShadeColor);
2691 if (twittext.length) {
2692 int tx = gxClipRect.x0+(gxClipRect.width-gxTextWidthScaledUtf(2, twittext))/2-1;
2693 int ty = gxClipRect.y0+(gxClipRect.height-3*gxTextHeightUtf)/2-1;
2694 gxDrawTextOutScaledUtf(2, tx, ty, twittext, twithTextColor, twithTextOutlineColor);
2699 // ////////////////////////////////////////////////////////////////// //
2700 void drawThreadList () {
2701 uint tid = 0;
2702 if (folderCurrIndex >= 0 && folderCurrIndex < folderList.length) tid = folderList[folderCurrIndex].tagid;
2703 switchToFolderTid(tid);
2705 // find indicies
2706 int topidx, curridx;
2707 getAndFixThreadListIndicies(out topidx, out curridx);
2709 int hgt = (guiThreadListHeight+gxTextHeightUtf-1)/gxTextHeightUtf;
2710 if (hgt < 1) hgt = 1;
2712 gxClipRect.x0 = guiGroupListWidth+2;
2713 gxClipRect.x1 = screenWidth-1-5;
2714 gxClipRect.y0 = 0;
2715 gxClipRect.y1 = guiThreadListHeight-1;
2716 immutable uint origX0 = gxClipRect.x0;
2717 immutable uint origX1 = gxClipRect.x1;
2718 immutable uint origY0 = gxClipRect.y0;
2719 immutable uint origY1 = gxClipRect.y1;
2720 int y = 0;
2722 chiroGetPaneTablePage(topidx, hgt,
2723 delegate (int pgofs, /* offset from the page start, from zero and up to `limit` */
2724 int iid, /* item id, never zero */
2725 uint uid, /* msguid, never zero */
2726 uint parentuid, /* parent msguid, may be zero */
2727 uint level, /* threading level, from zero */
2728 Appearance appearance, /* see above */
2729 Mute mute, /* see above */
2730 SQ3Text date, /* string representation of receiving date and time */
2731 SQ3Text subj, /* message subject, can be empty string */
2732 SQ3Text fromName, /* message sender name, can be empty string */
2733 SQ3Text fromMail, /* message sender email, can be empty string */
2734 SQ3Text title) /* title from twiting */
2736 import std.format : format;
2737 import std.datetime;
2739 if (y >= guiThreadListHeight) return;
2740 if (subj.length == 0) subj = "no subject";
2742 gxClipRect.x0 = origX0;
2743 gxClipRect.x1 = origX1;
2745 //auto art = abase[alist[idx]];
2747 //conwriteln(idx, " : ", fld.list.length);
2748 if (uid == msglistCurrUId) {
2749 uint cc = gxRGB!(0, 127, 127);
2750 if (isSoftDeleted(appearance)) cc = gxRGB!(0, 127-30, 127-30);
2751 gxFillRect(gxClipRect.x0, y, gxClipRect.width-1, gxTextHeightUtf, cc);
2753 gxClipRect.x0 = gxClipRect.x0+1;
2754 gxClipRect.x1 = gxClipRect.x1-1;
2756 //uint clr = (idx != fld.curidx ? gxRGB!(255, 127, 0) : gxRGB!(255, 255, 255));
2757 // `clr1` is "twitted color"
2758 uint clr = (appearance == Appearance.Unread ? gxRGB!(255, 255, 255) : gxRGB!(255-40, 127-40, 0));
2759 uint clr1 = (appearance == Appearance.Unread ? gxRGB!(255, 255, 0) : gxRGB!(255-60-40, 127-60-40, 0));
2761 if (level != 0 && appearance != Appearance.Unread) {
2762 clr = gxRGB!(255-90, 127-90, 0);
2763 clr1 = gxRGB!(255-90-40, 127-90-40, 0);
2766 // !read, !softdeleted
2767 if (appearance == Appearance.Read) {
2768 //FIXME:if (fld.isTwittedNL(idx) !is null) { clr = gxRGB!(60, 0, 0); clr1 = gxRGB!(60, 0, 0); }
2769 //FIXME:if (fld.isHighlightedNL(idx)) { clr = gxRGB!(0, 190, 0); clr1 = gxRGB!(0, 190-40, 0); }
2770 if (mute > Mute.Normal) { clr = gxRGB!(60, 0, 0); clr1 = gxRGB!(60, 0, 0); }
2771 } else if (isSoftDeleted(appearance)) {
2772 // soft-deleted
2773 //clr = gxRGB!(255-80, 127-80, 0);
2774 //clr1 = gxRGB!(255-80-40, 127-80-40, 0);
2775 if (appearance == Appearance.SoftDeletePurge) {
2776 clr = clr1 = gxRGB!(255, 0, 0);
2777 } else {
2778 clr = clr1 = gxRGB!(127, 0, 0);
2782 //auto t = SysTime.fromUnixTime(art.time);
2783 //string s = "%04d/%02d/%02d %02d:%02d".format(t.year, t.month, t.day, t.hour, t.minute);
2784 int timewdt;
2786 import core.stdc.stdio : snprintf;
2787 char[128] tmpbuf;
2788 //string s = "%02d/%02d %02d:%02d".format(t.month, t.day, t.hour, t.minute);
2789 //auto len = snprintf(tmpbuf.ptr, tmpbuf.length, "%02d/%02d/%02d %02d:%02d", t.year%100, t.month, t.day, t.hour, t.minute);
2790 //timewdt = gxDrawTextUtf(gxClipRect.x1-gxTextWidthUtf(tmpbuf[0..len]), y, tmpbuf[0..len], clr);
2791 timewdt = gxDrawTextUtf(gxClipRect.x1-gxTextWidthUtf(date), y, date, clr);
2793 if (timewdt%8) timewdt = (timewdt|7)+1;
2795 SQ3Text from = fromName;
2797 auto vp = from.indexOf(" via Digitalmars-");
2798 if (vp > 0) {
2799 from = from[0..vp].xstrip;
2800 if (from.length == 0) from = "anonymous";
2804 gxClipRect.x1 = gxClipRect.x1-/*(13*6+4)*2+33*/timewdt;
2805 enum FromWidth = 22*6*2+88;
2806 gxDrawTextUtf(gxClipRect.x1-FromWidth, y, from, clr);
2807 gxDrawTextUtf(gxClipRect.x1-FromWidth+gxTextWidthUtf(from)+4, y, "<", clr1);
2808 gxDrawTextUtf(gxClipRect.x1-FromWidth+gxTextWidthUtf(from)+4+gxTextWidthUtf("<")+1, y, fromMail, clr1);
2809 gxDrawTextUtf(gxClipRect.x1-FromWidth+gxTextWidthUtf(from)+4+gxTextWidthUtf("<")+1+gxTextWidthUtf(fromMail)+1, y, ">", clr1);
2811 gxClipRect.x1 = gxClipRect.x1-FromWidth-6;
2812 gxDrawTextUtf(gxClipRect.x0+level*3, y, subj, clr);
2813 foreach (immutable dx; 0..level) gxPutPixel(gxClipRect.x0+1+dx*3, y+gxTextHeightUtf/2, gxRGB!(70, 70, 70));
2815 // soft-deleted
2816 if (isSoftDeleted(appearance)) {
2817 gxClipRect.x0 = origX0;
2818 gxClipRect.x1 = origX1;
2819 gxHLine(gxClipRect.x0, y+gxTextHeightUtf/2, gxClipRect.x1-gxClipRect.x0+1, clr);
2820 if (appearance == Appearance.SoftDeletePurge) {
2821 gxHLine(gxClipRect.x0, y+gxTextHeightUtf/2+1, gxClipRect.x1-gxClipRect.x0+1, clr);
2825 y += gxTextHeightUtf;
2829 // draw progressbar
2830 if (msglistCurrUId) {
2831 gxClipRect.x0 = origX0;
2832 gxClipRect.x1 = origX1+5;
2833 gxClipRect.y0 = origY0;
2834 gxClipRect.y1 = origY1;
2835 gxDrawScrollBar(GxRect(gxClipRect.x1-5, gxClipRect.y0, 4, gxClipRect.height-1),
2836 cast(int)chiroGetTreePaneTableCount()-1, curridx);
2839 drawArticle(msglistCurrUId);
2842 // ////////////////////////////////////////////////////////////////// //
2843 void drawFolders () {
2844 folderMakeCurVisible();
2845 int ofsx = 2;
2846 int ofsy = 1;
2847 foreach (immutable idx, const FolderInfo fi; folderList) {
2848 if (idx < folderTopIndex) continue;
2849 if (ofsy >= screenHeight) break;
2850 gxClipReset();
2851 if (idx == folderCurrIndex) gxFillRect(0, ofsy-1, guiGroupListWidth, (gxTextHeightUtf+2), gxRGB!(0, 127, 127));
2852 gxClipRect.x0 = ofsx-1;
2853 gxClipRect.y0 = ofsy;
2854 gxClipRect.x1 = guiGroupListWidth-3;
2855 int depth = fi.depth;
2856 uint clr = gxRGB!(255-30, 127-30, 0);
2857 if (fi.unreadCount) {
2858 clr = gxRGB!(0, 255, 255);
2859 } else if (depth == 0) {
2860 if (fi.name == "#spam") clr = gxRGB!(128, 0, 0);
2861 else clr = (fi.name == "/accounts" ? gxRGB!(220, 220, 0) : gxRGB!(255, 127+60, 0));
2862 } else if (depth == 1 && fi.name.startsWith("/accounts/")) {
2863 //clr = gxRGB!(220, 220, 220);
2864 clr = gxRGB!(90+70, 90+70, 180+70);
2865 } else if (fi.name.startsWith("/accounts/") && fi.name.endsWith("/inbox")) {
2866 //clr = gxRGB!(255, 127+30, 0);
2867 clr = gxRGB!(90, 90, 180);
2869 foreach (immutable dd; 0..depth) gxPutPixel(ofsx+dd*6+2, ofsy+gxTextHeightUtf/2, gxRGB!(80, 80, 80));
2870 gxDrawTextOutScaledUtf(1, ofsx+depth*6, ofsy, fi.visname, clr, gxRGB!(0, 0, 0));
2871 ofsy += gxTextHeightUtf+2;
2875 drawFolders();
2876 drawThreadList();
2877 //setupTrayAnimation();
2880 override bool onKey (KeyEvent event) {
2881 if (event.pressed) {
2882 if (dbg_dump_keynames) conwriteln("key: ", event.toStr, ": ", event.modifierState&ModifierState.windows);
2883 //foreach (const ref kv; mainAppKeyBindings.byKeyValue) if (event == kv.key) concmd(kv.value);
2884 char[64] kname;
2885 if (auto cmdp = event.toStrBuf(kname[]) in mainAppKeyBindings) {
2886 concmd(*cmdp);
2887 return true;
2889 // debug
2891 if (event == "S-Up") {
2892 if (folderTop > 0) --folderTop;
2893 postScreenRebuild();
2894 return true;
2896 if (event == "S-Down") {
2897 if (folderTop+1 < folders.length) ++folderTop;
2898 postScreenRebuild();
2899 return true;
2902 //if (event == "Tab") { new PostWindow(); return true; }
2903 //if (event == "Tab") { new SelectPopBoxWindow(null); return true; }
2905 return false;
2908 // returning `false` to avoid screen rebuilding by dispatcher
2909 override bool onMouse (MouseEvent event) {
2910 //FIXME: use window coordinates
2911 int mx, my;
2912 event.mouse2xy(mx, my);
2913 // button press
2914 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
2915 // select folder
2916 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < screenHeight) {
2917 uint fnum = my/(gxTextHeightUtf+2)+folderTopIndex;
2918 if (fnum >= 0 && fnum != folderCurrIndex && folderCurrIndex < folderList.length) {
2919 folderCurrIndex = fnum;
2920 postScreenRebuild();
2922 return false;
2924 // select post
2925 if (mx > guiGroupListWidth && mx < screenWidth && my >= 0 && my < guiThreadListHeight) {
2926 if (lastDecodedTid != 0) {
2927 my /= gxTextHeightUtf;
2928 // find indicies
2929 int topidx, curridx;
2930 getAndFixThreadListIndicies(out topidx, out curridx);
2931 int newidx = topidx+my;
2932 if (curridx != newidx) {
2933 uint newuid = chiroGetTreePaneTableIndex2Uid(newidx);
2934 if (newuid && newuid != msglistCurrUId) {
2935 chiroSetMessageRead(lastDecodedTid, newuid);
2936 msglistCurrUId = newuid;
2937 setupTrayAnimation();
2938 postScreenRebuild();
2941 return false;
2945 // wheel
2946 if (event.type == MouseEventType.buttonPressed && (event.button == MouseButton.wheelUp || event.button == MouseButton.wheelDown)) {
2947 // folder
2948 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < screenHeight) {
2949 if (event.button == MouseButton.wheelUp) {
2950 if (folderCurrIndex > 0) {
2951 --folderCurrIndex;
2952 postScreenRebuild();
2954 } else {
2955 if (folderCurrIndex+1 < folderList.length) {
2956 ++folderCurrIndex;
2957 postScreenRebuild();
2960 return false;
2962 // post
2963 if (mx > guiGroupListWidth && mx < screenWidth && my >= 0 && my < guiThreadListHeight) {
2964 if (event.button == MouseButton.wheelUp) {
2965 if (threadListUp()) postScreenRebuild();
2966 } else {
2967 if (threadListDown()) postScreenRebuild();
2969 return false;
2971 // text
2972 if (mx > guiGroupListWidth && mx < screenWidth && my > guiThreadListHeight && my < screenHeight) {
2973 enum ScrollLines = 2;
2974 if (event.button == MouseButton.wheelUp) scrollBy(-ScrollLines); else scrollBy(ScrollLines);
2975 postScreenRebuild();
2976 return false;
2979 // button release
2980 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
2981 // try url
2982 auto url = findUrlAt(mx, my);
2983 if (url !is null) {
2984 if (url.isAttach) {
2985 if (event.modifierState&(ModifierState.alt|ModifierState.shift|ModifierState.ctrl)) {
2986 concmdf!"attach_save %s"(url.attachnum);
2987 } else {
2988 concmdf!"attach_view %s"(url.attachnum);
2990 } else {
2991 if (event.modifierState&(ModifierState.shift|ModifierState.ctrl)) {
2992 //conwriteln("link-to-clipboard: <", url.url, ">");
2993 setClipboardText(vbwin, url.url); // it is safe to cast here
2994 setPrimarySelection(vbwin, url.url); // it is safe to cast here
2995 conwriteln("url copied to the clipboard.");
2996 } else {
2997 //conwriteln("link-open: <", url.url, ">");
2998 concmdf!"open_url \"%s\" %s"(url.url, ((event.modifierState&ModifierState.alt) != 0));
3003 if (event.type == MouseEventType.motion) {
3004 auto uidx = findUrlIndexAt(mx, my);
3005 if (uidx != lastUrlIndex) { lastUrlIndex = uidx; postScreenRebuild(); return false; }
3008 if (event.type == MouseEventType.buttonPressed || event.type == MouseEventType.buttonReleased) {
3009 postScreenRebuild();
3010 } else {
3011 // for OpenGL, this rebuilds the whole screen anyway
3012 postScreenRepaint();
3015 return false;
3018 override bool onChar (dchar ch) {
3019 return false;
3024 // ////////////////////////////////////////////////////////////////////////// //
3025 // call this from both backend handlers
3026 void egfxRepaint (SimpleWindow w, immutable bool fromGLHandler=false) {
3027 if (w is null || w.closed) return;
3029 bool resizeWin = false;
3032 consoleLock();
3033 scope(exit) consoleUnlock();
3035 if (!conQueueEmpty()) postDoConCommands();
3037 if (vbufNewScale != screenEffScale) {
3038 // window scale changed
3039 resizeWin = true;
3041 if (vbufEffVSync != vbufVSync) {
3042 vbufEffVSync = vbufVSync;
3043 w.vsync = vbufEffVSync;
3047 if (resizeWin) {
3048 w.resize(screenWidthScaled, screenHeightScaled);
3049 glconResize(screenWidthScaled, screenHeightScaled);
3050 //vglResizeBuffer(screenWidth, screenHeight, vbufNewScale);
3053 //if (rebuildTexture) repaintScreen();
3055 gxClipReset();
3056 gxClearScreen(0);
3057 paintSubWindows();
3058 vglUpdateTexture(); // this does nothing for X11, but required for OpenGL
3060 static if (EGfxOpenGLBackend) {
3061 if (!fromGLHandler) w.setAsCurrentOpenGlContext();
3063 scope(exit) {
3064 static if (EGfxOpenGLBackend) {
3065 if (!fromGLHandler) w.releaseCurrentOpenGlContext();
3069 static if (EGfxOpenGLBackend) {
3070 vglBlitTexture();
3071 } else {
3072 vglBlitTexture(w);
3075 if (vArrowTextureId) {
3076 if (isMouseVisible) {
3077 int px = lastMouseX;
3078 int py = lastMouseY;
3079 //conwriteln("msalpha: ", mouseAlpha);
3080 static if (EGfxOpenGLBackend) {
3081 glColor4f(1, 1, 1, mouseAlpha);
3083 vglBlitArrow(px, py);
3087 static if (EGfxOpenGLBackend) {
3088 glconDrawWindow = null;
3089 glconDrawDirect = false;
3090 glconDraw();
3091 } else {
3093 auto painter = w.draw();
3094 vglBlitTexture(w);
3095 glconDrawWindow = w;
3096 glconDrawDirect = false;
3097 glconDraw();
3098 glconDrawWindow = null;
3099 //painter.drawImage(Point(0, 0), egx11img);
3103 if (isQuitRequested()) w.postEvent(new QuitEvent());
3107 void rebuildScreen (SimpleWindow w) {
3108 if (w !is null && !w.closed && !w.hidden) {
3109 static if (EGfxOpenGLBackend) {
3110 w.redrawOpenGlSceneNow();
3111 } else {
3112 egfxRepaint(w);
3113 //flushGui();
3119 void repaintScreen (SimpleWindow w) {
3120 if (w !is null && !w.closed && !w.hidden) {
3121 static if (EGfxOpenGLBackend) {
3122 w.redrawOpenGlSceneNow();
3123 //flushGui();
3124 } else {
3125 static __gshared bool lastvisible = false;
3126 bool curvisible = isConsoleVisible;
3127 if (lastvisible != curvisible || curvisible) {
3128 lastvisible = curvisible;
3129 rebuildScreen(w);
3130 return;
3137 // ////////////////////////////////////////////////////////////////////////// //
3138 __gshared LockFile mainLockFile;
3141 void checkMainLockFile () {
3142 import std.path : buildPath;
3143 mainLockFile = LockFile(buildPath(mailRootDir, ".chiroptera2.lock"));
3144 if (!mainLockFile.tryLock) {
3145 mainLockFile.close();
3146 //assert(0, "already running");
3147 conwriteln("another copy of Chiroptera is running, disabling updater.");
3148 receiverDisable();
3153 void main (string[] args) {
3155 import etc.linux.memoryerror;
3156 bool setMH = true;
3157 int idx = 1;
3158 while (idx < args.length) {
3159 string a = args[idx++];
3160 if (a == "--") break;
3161 if (a == "--gdb") {
3162 setMH = false;
3163 --idx;
3164 foreach (immutable c; idx+1..args.length) args[c-1] = args[c];
3167 if (setMH) registerMemoryErrorHandler();
3170 glconAllowOpenGLRender = false;
3172 checkMainLockFile();
3173 scope(exit) mainLockFile.close();
3175 sdpyWindowClass = "Chiroptera";
3176 //glconShowKey = "M-Grave";
3178 initConsole();
3179 //FIXME
3180 //hitwitInitConsole();
3182 clearBindings();
3183 setupDefaultBindings();
3185 concmd("exec chiroptera.rc tan");
3187 //FIXME
3188 //scanFolders();
3190 //FIXME:concmdf!"exec %s/accounts.rc tan"(mailRootDir);
3191 //FIXME:concmdf!"exec %s/addressbook.rc tan"(mailRootDir);
3192 //FIXME:concmdf!"exec %s/filters.rc tan"(mailRootDir);
3193 //FIXME:concmdf!"exec %s/highlights.rc tan"(mailRootDir);
3194 //FIXME:concmdf!"exec %s/twits.rc tan"(mailRootDir);
3195 //FIXME:concmdf!"exec %s/twit_threads.rc tan"(mailRootDir);
3196 //FIXME:concmdf!"exec %s/auto_twits.rc tan"(mailRootDir);
3197 //FIXME:concmdf!"exec %s/auto_twit_threads.rc tan"(mailRootDir);
3198 //FIXME:concmdf!"exec %s/repreps.rc tan"(mailRootDir);
3199 conProcessQueue(); // load config
3200 conProcessArgs!true(args);
3202 chiroOpenStorageDB();
3203 chiroOpenViewDB();
3204 chiroOpenConfDB();
3205 //ChiroTimerEnabled = true;
3206 //ChiroTimerExEnabled = true;
3208 rescanFolders();
3210 screenEffScale = vbufNewScale;
3211 vbufEffVSync = vbufVSync;
3213 lastWinWidth = screenWidthScaled;
3214 lastWinHeight = screenHeightScaled;
3216 //FIXME
3217 //restoreCurrentFolderAndPosition();
3219 static if (EGfxOpenGLBackend) {
3220 vbwin = new SimpleWindow(lastWinWidth, lastWinHeight, "Chiroptera", OpenGlOptions.yes, Resizability.allowResizing);
3221 vbwin.hideCursor();
3222 glconAllowOpenGLRender = true;
3223 } else {
3224 vbwin = new SimpleWindow(lastWinWidth, lastWinHeight, "Chiroptera", OpenGlOptions.no, Resizability.allowResizing);
3227 vbwin.onFocusChange = delegate (bool focused) {
3228 vbfocused = focused;
3229 if (!focused) {
3230 lastMouseButton = 0;
3231 eguiLostGlobalFocus();
3235 vbwin.windowResized = delegate (int wdt, int hgt) {
3236 // TODO: fix gui sizes
3237 if (vbwin.closed) return;
3239 if (lastWinWidth == wdt && lastWinHeight == hgt) return;
3240 glconResize(wdt, hgt);
3242 double glwFrac = cast(double)guiGroupListWidth/screenWidth;
3243 double tlhFrac = cast(double)guiThreadListHeight/screenHeight;
3245 if (wdt < vbufNewScale*32) wdt = vbufNewScale;
3246 if (hgt < vbufNewScale*32) hgt = vbufNewScale;
3247 int newwdt = (wdt+vbufNewScale-1)/vbufNewScale;
3248 int newhgt = (hgt+vbufNewScale-1)/vbufNewScale;
3250 guiGroupListWidth = cast(int)(glwFrac*newwdt+0.5);
3251 guiThreadListHeight = cast(int)(tlhFrac*newhgt+0.5);
3253 if (guiGroupListWidth < 12) guiGroupListWidth = 12;
3254 if (guiThreadListHeight < 16) guiThreadListHeight = 16;
3256 lastWinWidth = wdt;
3257 lastWinHeight = hgt;
3259 vglResizeBuffer(newwdt, newhgt, vbufNewScale);
3261 mouseMoved();
3263 rebuildScreen(vbwin);
3266 vbwin.addEventListener((DoConsoleCommandsEvent evt) {
3267 bool sendAnother = false;
3268 bool prevVisible = isConsoleVisible;
3270 consoleLock();
3271 scope(exit) consoleUnlock();
3272 conProcessQueue();
3273 sendAnother = !conQueueEmpty();
3275 if (sendAnother) postDoConCommands();
3276 if (vbwin.closed) return;
3277 if (isQuitRequested) { vbwin.close(); return; }
3278 if (prevVisible || isConsoleVisible) postScreenRepaintDelayed();
3281 vbwin.addEventListener((HideMouseEvent evt) {
3282 if (vbwin.closed) return;
3283 if (isQuitRequested) { vbwin.close(); return; }
3284 if (repostHideMouse) {
3285 //conwriteln("HME: visible=", isMouseVisible, "; alpha=", mouseAlpha, "; tm=", mshtime_dbg);
3286 if (mainPane !is null && !isMouseVisible) mainPane.lastUrlIndex = -1;
3287 repaintScreen(vbwin);
3291 vbwin.addEventListener((ScreenRebuildEvent evt) {
3292 if (vbwin.closed) return;
3293 if (isQuitRequested) { vbwin.close(); return; }
3294 rebuildScreen(vbwin);
3295 if (isConsoleVisible) postScreenRepaintDelayed();
3298 vbwin.addEventListener((ScreenRepaintEvent evt) {
3299 if (vbwin.closed) return;
3300 if (isQuitRequested) { vbwin.close(); return; }
3301 repaintScreen(vbwin);
3302 if (isConsoleVisible) postScreenRepaintDelayed();
3305 vbwin.addEventListener((CursorBlinkEvent evt) {
3306 if (vbwin.closed) return;
3307 rebuildScreen(vbwin);
3310 vbwin.addEventListener((QuitEvent evt) {
3311 if (vbwin.closed) return;
3312 if (isQuitRequested) { vbwin.close(); return; }
3313 vbwin.close();
3317 vbwin.addEventListener((TrayAnimationStepEvent evt) {
3318 if (vbwin.closed) return;
3319 if (isQuitRequested) { vbwin.close(); return; }
3320 trayDoAnimationStep();
3323 HintWindow uphintWindow;
3325 vbwin.addEventListener((UpdatingAccountEvent evt) {
3326 DynStr accName = chiroGetAccountName(evt.accid);
3327 if (accName.length) {
3328 DynStr msg = "updating: ";
3329 msg ~= accName;
3330 if (uphintWindow !is null) {
3331 uphintWindow.message = msg;
3332 } else {
3333 uphintWindow = new HintWindow(msg);
3334 uphintWindow.winy = guiThreadListHeight+1+(3*gxTextHeightUtf+2-uphintWindow.winh)/2;
3336 postScreenRebuild();
3340 vbwin.addEventListener((UpdatingAccountCompleteEvent evt) {
3341 if (uphintWindow is null) return;
3342 DynStr accName = chiroGetAccountName(evt.accid);
3343 if (accName.length) {
3344 DynStr msg = "done: ";
3345 msg ~= accName;
3346 uphintWindow.message = msg;
3347 postScreenRebuild();
3351 vbwin.addEventListener((UpdatingCompleteEvent evt) {
3352 if (uphintWindow) {
3353 uphintWindow.close();
3354 uphintWindow = null;
3356 if (vbwin is null || vbwin.closed) return;
3357 setupTrayAnimation(); // check if we have to start/stop animation, and do it
3358 postScreenRebuild();
3361 vbwin.addEventListener((TagThreadsUpdatedEvent evt) {
3362 if (vbwin is null || vbwin.closed) return;
3363 if (mainPane is null) return;
3364 if (evt.tagid && mainPane.lastDecodedTid == evt.tagid) {
3365 // force view pane rebuild
3366 mainPane.switchToFolderTid(evt.tagid, forced:true);
3367 postScreenRebuild();
3371 MessageWindow recalcHintWindow;
3373 vbwin.addEventListener((RecalcAllTwitsEvent evt) {
3374 if (vbwin !is null && !vbwin.closed) {
3375 glconHide();
3376 if (recalcHintWindow !is null) recalcHintWindow.close();
3377 recalcHintWindow = new MessageWindow("recalculating twits");
3378 rebuildScreen(vbwin);
3379 } else {
3380 recalcHintWindow = null;
3383 disableMailboxUpdates();
3384 scope(exit) enableMailboxUpdates();
3385 chiroRecalcAllTwits((msg, curr, total) {
3386 if (recalcHintWindow is null) return;
3387 if (recalcHintWindow.setProgress(msg, curr, total)) {
3388 rebuildScreen(vbwin);
3392 if (vbwin !is null && !vbwin.closed) {
3393 if (recalcHintWindow !is null) recalcHintWindow.close();
3394 if (mainPane !is null) mainPane.switchToFolderTid(mainPane.lastDecodedTid, forced:true);
3395 postScreenRebuild();
3399 vbwin.addEventListener((ArticleTextScrollEvent evt) {
3400 if (vbwin is null || vbwin.closed) return;
3401 if (mainPane is null) return;
3402 mainPane.doScrollStep();
3405 vbwin.addEventListener((MarkAsUnreadEvent evt) {
3406 if (vbwin is null || vbwin.closed) return;
3407 if (mainPane is null) return;
3408 //conwriteln("unread timer fired");
3409 if (mainPane.lastDecodedTid == evt.tagid && evt.uid == mainPane.msglistCurrUId) {
3410 //conwriteln("*** unread timer hit!");
3411 chiroSetMessageRead(evt.tagid, evt.uid);
3412 setupTrayAnimation();
3413 postScreenRebuild();
3417 vbwin.redrawOpenGlScene = delegate () {
3418 if (vbwin.closed) return;
3419 egfxRepaint(vbwin, fromGLHandler:true);
3422 static if (is(typeof(&vbwin.closeQuery))) {
3423 vbwin.closeQuery = delegate () { concmd("quit"); postDoConCommands(); };
3426 void firstTimeInit () {
3427 static bool firstTimeInited = false;
3428 if (firstTimeInited) return;
3429 firstTimeInited = true;
3431 static if (EGfxOpenGLBackend) {
3432 import iv.glbinds;
3433 vbwin.setAsCurrentOpenGlContext();
3434 vbwin.vsync = vbufEffVSync;
3436 vbufEffVSync = vbufVSync;
3438 vglResizeBuffer(screenWidth, screenHeight);
3439 vglCreateArrowTexture();
3441 glconInit(screenWidthScaled, screenHeightScaled);
3443 rebuildScreen(vbwin);
3445 //FIXME
3446 //updateThreadId = spawn(&updateThread, thisTid);
3448 // create notification icon
3449 if (trayicon is null) {
3450 auto drv = vfsAddPak(wrapMemoryRO(iconsZipData[]), "", "databinz/icons.zip:");
3451 scope(exit) vfsRemovePak(drv);
3452 try {
3453 foreach (immutable idx; 0..6) {
3454 string fname = "databinz/icons.zip:icons";
3455 if (idx == 0) {
3456 fname ~= "/main.png";
3457 } else {
3458 import std.format : format;
3459 fname = "%s/bat%s.png".format(fname, idx-1);
3461 auto fl = VFile(fname);
3462 if (fl.size == 0 || fl.size > 1024*1024) throw new Exception("fucked icon");
3463 auto pngraw = new ubyte[](cast(uint)fl.size);
3464 fl.rawReadExact(pngraw);
3465 auto img = readPng(pngraw);
3466 if (img is null) throw new Exception("fucked icon");
3467 icons[idx] = imageFromPng(img);
3469 foreach (immutable idx, MemoryImage img; icons[]) {
3470 trayimages[idx] = Image.fromMemoryImage(img);
3472 vbwin.icon = icons[0];
3473 trayicon = new NotificationAreaIcon("Chiroptera", trayimages[0], (MouseButton button) {
3474 scope(exit) if (!conQueueEmpty()) postDoConCommands();
3475 if (button == MouseButton.left) vbwin.switchToWindow();
3476 if (button == MouseButton.middle) concmd("quit");
3478 setupTrayAnimation();
3479 flushGui(); // or it may not redraw itself
3480 } catch (Exception e) {
3481 conwriteln("ERROR loading icons: ", e.msg);
3486 vbwin.visibleForTheFirstTime = delegate () {
3487 firstTimeInit();
3490 mainPane = new MainPaneWindow();
3492 postScreenRebuild();
3493 repostHideMouse();
3495 receiverInit();
3497 vbwin.eventLoop(1000*10,
3498 delegate () {
3499 scope(exit) if (!conQueueEmpty()) postDoConCommands();
3500 if (vbwin.closed) return;
3502 consoleLock();
3503 scope(exit) consoleUnlock();
3504 conProcessQueue();
3506 if (isQuitRequested) { vbwin.close(); return; }
3507 if (mainPane !is null) {
3508 mainPane.checkSaveState();
3509 setupTrayAnimation();
3512 delegate (KeyEvent event) {
3513 scope(exit) if (!conQueueEmpty()) postDoConCommands();
3514 if (vbwin.closed) return;
3515 if (isQuitRequested) { vbwin.close(); return; }
3516 if (glconKeyEvent(event)) {
3517 postScreenRepaint();
3518 return;
3520 if ((event.modifierState&ModifierState.numLock) == 0) {
3521 switch (event.key) {
3522 case Key.Pad0: event.key = Key.Insert; break;
3523 case Key.Pad1: event.key = Key.End; break;
3524 case Key.Pad2: event.key = Key.Down; break;
3525 case Key.Pad3: event.key = Key.PageDown; break;
3526 case Key.Pad4: event.key = Key.Left; break;
3527 //case Key.Pad5: event.key = Key.Insert; break;
3528 case Key.Pad6: event.key = Key.Right; break;
3529 case Key.Pad7: event.key = Key.Home; break;
3530 case Key.Pad8: event.key = Key.Up; break;
3531 case Key.Pad9: event.key = Key.PageUp; break;
3532 case Key.PadEnter: event.key = Key.Enter; break;
3533 case Key.PadDot: event.key = Key.Delete; break;
3534 default: break;
3536 } else {
3537 if (event.key == Key.PadEnter) event.key = Key.Enter;
3539 if (dispatchEvent(event)) return;
3540 //postScreenRepaint(); // just in case
3542 delegate (MouseEvent event) {
3543 scope(exit) if (!conQueueEmpty()) postDoConCommands();
3544 if (vbwin.closed) return;
3545 lastMouseXUnscaled = event.x;
3546 lastMouseYUnscaled = event.y;
3547 if (event.type == MouseEventType.buttonPressed) lastMouseButton |= event.button;
3548 else if (event.type == MouseEventType.buttonReleased) lastMouseButton &= ~event.button;
3549 mouseMoved();
3550 if (dispatchEvent(event)) return;
3552 delegate (dchar ch) {
3553 if (vbwin.closed) return;
3554 scope(exit) if (!conQueueEmpty()) postDoConCommands();
3555 if (glconCharEvent(ch)) {
3556 postScreenRepaint();
3557 return;
3559 if (dispatchEvent(ch)) return;
3563 mainPane.saveCurrentState();
3565 trayimages[] = null;
3566 if (trayicon !is null && !trayicon.closed) { trayicon.close(); trayicon = null; }
3567 flushGui();
3568 receiverDeinit();
3569 conProcessQueue(int.max/4);