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