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