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