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*/;
21 import std
.concurrency
;
24 //import arsd.htmltotext;
25 import arsd
.simpledisplay
;
52 // ////////////////////////////////////////////////////////////////////////// //
53 __gshared
ubyte vbufNewScale
= 1; // new window scale
54 __gshared
int lastWinWidth
, lastWinHeight
;
55 __gshared
bool vbufVSync
= false;
56 __gshared
bool vbufEffVSync
= false;
58 __gshared NotificationAreaIcon trayicon
;
59 __gshared Image
[6] trayimages
;
60 __gshared MemoryImage
[6] icons
; // 0: normal
63 // ////////////////////////////////////////////////////////////////////////// //
64 static immutable ubyte[] iconsZipData
= cast(immutable(ubyte)[])import("databin/icons.zip");
67 // ////////////////////////////////////////////////////////////////////////// //
68 struct MailReplacement
{
74 __gshared MailReplacement
[string
] repmailreps
;
77 // ////////////////////////////////////////////////////////////////////////// //
78 string
getBrowserCommand (bool forceOpera
) {
79 __gshared string browser
;
80 if (forceOpera
) return "opera";
81 if (browser
.length
== 0) {
82 import core
.stdc
.stdlib
: getenv
;
83 const(char)* evar
= getenv("BROWSER");
84 if (evar
!is null && evar
[0]) {
85 import std
.string
: fromStringz
;
86 browser
= evar
.fromStringz
.idup
;
95 // ////////////////////////////////////////////////////////////////////////// //
96 __gshared
bool dbg_dump_keynames
;
99 // ////////////////////////////////////////////////////////////////////////// //
100 class TrayAnimationStepEvent
{}
101 __gshared TrayAnimationStepEvent evTrayAnimationStep
;
102 shared static this () { evTrayAnimationStep
= new TrayAnimationStepEvent(); }
104 __gshared Tid updateThreadId
;
105 shared bool updateInProgress
= false;
106 __gshared
int trayAnimationIndex
= 0; // 0: no animation
107 __gshared
int trayAnimationDir
= 1; // direction
110 // ////////////////////////////////////////////////////////////////////////// //
111 void trayPostAnimationEvent () {
112 if (vbwin
!is null && !vbwin
.eventQueued
!TrayAnimationStepEvent
) vbwin
.postTimeout(evTrayAnimationStep
, 100);
116 void trayDoAnimationStep () {
117 if (trayicon
is null || trayicon
.closed
) return; // no tray icon
118 if (vbwin
is null || vbwin
.closed
) return;
119 if (trayAnimationIndex
== 0) return; // no animation
120 trayPostAnimationEvent();
121 if (trayAnimationDir
< 0) {
122 if (--trayAnimationIndex
== 1) trayAnimationDir
= 1;
124 if (++trayAnimationIndex
== trayimages
.length
-1) trayAnimationDir
= -1;
126 trayicon
.icon
= trayimages
[trayAnimationIndex
];
127 //vbwin.icon = icons[trayAnimationIndex];
128 //vbwin.sendDummyEvent(); // or it won't redraw itself
129 //flushGui(); // or it may not redraw itself
133 // ////////////////////////////////////////////////////////////////////////// //
134 void trayStartAnimation () {
135 if (trayicon
is null) return; // no tray icon
136 if (trayAnimationIndex
== 0) {
137 trayAnimationIndex
= 1;
138 trayAnimationDir
= 1;
139 trayicon
.icon
= trayimages
[1];
140 vbwin
.icon
= icons
[1];
141 //vbwin.sendDummyEvent(); // or it may not redraw itself
142 flushGui(); // or it may not redraw itself
143 trayPostAnimationEvent();
148 void trayStopAnimation () {
149 if (trayicon
is null) return; // no tray icon
150 if (trayAnimationIndex
!= 0) {
151 trayAnimationIndex
= 0;
152 trayAnimationDir
= 1;
153 trayicon
.icon
= trayimages
[0];
154 vbwin
.icon
= icons
[0];
155 //vbwin.sendDummyEvent(); // or it may not redraw itself
156 flushGui(); // or it may not redraw itself
161 // check if we have to start/stop animation, and do it
162 void setupTrayAnimation () {
163 foreach (Folder
fld; folders
) {
164 if (fld.unreadCount
> 0) { trayStartAnimation(); return; }
170 // ////////////////////////////////////////////////////////////////////////// //
171 __gshared string
[string
] mainAppKeyBindings
;
173 void clearBindings () {
174 mainAppKeyBindings
.clear();
178 void mainAppBind (ConString kname
, ConString concmd
) {
179 KeyEvent evt
= KeyEvent
.parse(kname
);
181 mainAppKeyBindings
[evt
.toStr
] = concmd
.idup
;
183 mainAppKeyBindings
.remove(evt
.toStr
);
188 void mainAppUnbind (ConString kname
) {
189 KeyEvent evt
= KeyEvent
.parse(kname
);
190 mainAppKeyBindings
.remove(evt
.toStr
);
194 void setupDefaultBindings () {
195 //mainAppBind("C-L", "dbg_font_window");
196 mainAppBind("C-Q", "quit_prompt");
197 mainAppBind("N", "next_unread ona");
198 mainAppBind("S-N", "next_unread tan");
199 mainAppBind("M", "next_unread ona");
200 mainAppBind("S-M", "next_unread tan");
201 mainAppBind("U", "mark_unread");
202 mainAppBind("R", "mark_read");
203 mainAppBind("Space", "artext_page_down");
204 mainAppBind("S-Space", "artext_page_up");
205 mainAppBind("M-Up", "artext_line_up");
206 mainAppBind("M-Down", "artext_line_down");
207 mainAppBind("Up", "article_prev");
208 mainAppBind("Down", "article_next");
209 mainAppBind("PageUp", "article_pgup");
210 mainAppBind("PageDown", "article_pgdown");
211 mainAppBind("Home", "article_to_first");
212 mainAppBind("End", "article_to_last");
213 mainAppBind("C-Up", "article_scroll_up");
214 mainAppBind("C-Down", "article_scroll_down");
215 mainAppBind("C-PageUp", "folder_prev");
216 mainAppBind("C-PageDown", "folder_next");
217 mainAppBind("C-M-U", "folder_update");
218 mainAppBind("C-H", "article_dump_headers");
219 mainAppBind("C-S-I", "update_all");
220 mainAppBind("C-Backslash", "find_mine");
221 mainAppBind("C-Slash", "article_to_parent");
222 mainAppBind("C-Comma", "article_to_prev_sib");
223 mainAppBind("C-Period", "article_to_next_sib");
224 mainAppBind("C-Insert", "article_copy_url_to_clipboard");
225 mainAppBind("C-M-K", "article_twit_thread");
226 mainAppBind("T", "article_edit_poster_title");
227 mainAppBind("C-R", "article_reply");
228 mainAppBind("S-R", "article_reply_to_from");
229 mainAppBind("S-P", "new_post");
230 mainAppBind("S-Enter", "article_open_in_browser");
231 mainAppBind("M-Enter", "article_open_in_browser tan");
232 mainAppBind("Delete", "article_softdelete_toggle");
236 // ////////////////////////////////////////////////////////////////////////// //
237 enum UpThreadCommand
{
239 StartUpdate
, // start updating now
243 void updateThread (Tid ownerTid
) {
247 MonoTime lastCollect
= MonoTime
.currTime
;
249 receiveTimeout(30.seconds
,
250 (UpThreadCommand cmd
) {
252 case UpThreadCommand
.Ping
: break;
253 case UpThreadCommand
.StartUpdate
: break;
254 case UpThreadCommand
.Quit
: doQuit
= true; break;
259 bool updateProgressSet
= false;
260 foreach (Account acc
; accounts
) {
261 if (acc
.needUpdate
) {
262 updateProgressSet
= true;
263 atomicStore(updateInProgress
, true);
265 if (vbwin
!is null) vbwin
.postEvent(new UpdatingAccountEvent(acc
.name
));
267 } catch (Exception e
) {
268 conwriteln("ERROR UPDATING ACCOUNT '", acc
.name
, "': ", e
.msg
);
270 e
.toString(delegate (in char[] c
) { etrc
~= c
; });
274 if (vbwin
!is null) vbwin
.postEvent(new UpdatingAccountCompleteEvent(acc
.name
));
277 if (updateProgressSet
) {
278 if (vbwin
!is null) vbwin
.postEvent(new UpdatingCompleteEvent());
279 atomicStore(updateInProgress
, false);
282 auto ctt
= MonoTime
.currTime
;
283 if ((ctt
-lastCollect
).total
!"minutes" >= 5) {
284 import core
.memory
: GC
;
291 } catch (Throwable e
) {
292 // here, we are dead and fucked (the exact order doesn't matter)
293 import core
.stdc
.stdlib
: abort
;
294 import core
.stdc
.stdio
: fprintf
, stderr
;
295 import core
.memory
: GC
;
296 import core
.thread
: thread_suspendAll
;
297 GC
.disable(); // yeah
298 thread_suspendAll(); // stop right here, you criminal scum!
299 auto s
= e
.toString();
300 fprintf(stderr
, "\n=== FATAL ===\n%.*s\n", cast(uint)s
.length
, s
.ptr
);
301 abort(); // die, you bitch!
306 // ////////////////////////////////////////////////////////////////////////// //
307 void initConsole () {
308 import std
.functional
: toDelegate
;
310 //conRegVar!vbufNewScale(1, 4, "v_scale", "window scale: [1..3]");
312 conRegVar
!bool("v_vsync", "sync to video refresh rate?",
313 (ConVarBase self
) => vbufVSync
,
314 (ConVarBase self
, bool nv
) {
315 static if (EGfxOpenGLBackend
) {
316 if (vbufVSync
!= nv
) {
324 conRegVar
!scrollKeepLines("scroll_keep_lines", "number of lines to keep on page up or page down.");
325 conRegVar
!unreadTimeoutInterval("t_unread_timeout", "timout to mark keyboard-selected message as unread (<1: don't mark)");
327 conRegFunc
!clearBindings("binds_app_clear", "clear main application keybindings");
328 conRegFunc
!setupDefaultBindings("binds_app_default", "*append* default application bindings");
329 conRegFunc
!mainAppBind("bind_app", "add main application binding");
330 conRegFunc
!mainAppUnbind("unbind_app", "remove main application binding");
333 // //////////////////////////////////////////////////////////////////// //
335 import core
.memory
: GC
;
336 conwriteln("starting GC collection...");
339 conwriteln("GC collection complete.");
340 })("gc_collect", "force GC collection cycle");
343 // //////////////////////////////////////////////////////////////////// //
345 auto qww
= new YesNoWindow("Quit?", "Do you really want to quit?", true);
346 qww
.onYes
= () { concmd("quit"); };
348 })("quit_prompt", "quit with prompt");
351 // //////////////////////////////////////////////////////////////////// //
352 conRegFunc
!((ConString url
, bool forceOpera
=false) {
354 import std
.stdio
: File
;
357 auto frd
= File("/dev/null");
358 auto fwr
= File("/dev/null", "w");
359 spawnProcess([getBrowserCommand(forceOpera
), url
.idup
], frd
, fwr
, fwr
, null, Config
.detached
);
360 } catch (Exception e
) {
361 conwriteln("ERROR executing URL viewer (", e
.msg
, ")");
364 })("open_url", "open given url in a browser");
367 // //////////////////////////////////////////////////////////////////// //
369 foreach (Account acc
; accounts
) acc
.forceUpdating();
370 if (!atomicLoad(updateInProgress
)) {
371 updateThreadId
.send(UpThreadCommand
.StartUpdate
);
373 })("update_all", "mark all groups for updating");
376 // //////////////////////////////////////////////////////////////////// //
379 folderCur
= folderCur
-1;
380 if (mainPane
!is null) mainPane
.resetUnreadTimer();
383 })("folder_prev", "go to previous group");
386 if (folders
.length
-folderCur
> 1) {
387 folderCur
= folderCur
+1;
388 if (mainPane
!is null) mainPane
.resetUnreadTimer();
391 })("folder_next", "go to next group");
394 // //////////////////////////////////////////////////////////////////// //
396 if (auto fld = getActiveFolder
) {
397 if (fld.markAsUnread()) {
398 if (mainPane
!is null) mainPane
.resetUnreadTimer();
402 })("mark_unread", "mark current message as unread");
405 if (auto fld = getActiveFolder
) {
406 if (fld.markAsRead()) {
407 if (mainPane
!is null) mainPane
.resetUnreadTimer();
411 })("mark_read", "mark current message as read");
413 conRegFunc
!((bool allowNextGroup
=false) {
414 if (auto fld = getActiveFolder
) {
415 if (!fld.moveToNextUnread(true)) {
416 if (!allowNextGroup
) return;
418 uint fidx
= cast(uint)((folderCur
+1)%folders
.length
);
419 foreach (immutable _
; 0..folders
.length
) {
420 if (folders
[fidx
].moveToNextUnread(true)) {
422 if (mainPane
!is null) mainPane
.resetUnreadTimer();
426 fidx
= cast(uint)((fidx
+1)%folders
.length
);
432 })("next_unread", "move to next unread message; bool arg: can skip to next group?");
435 // //////////////////////////////////////////////////////////////////// //
437 if (mainPane
!is null) mainPane
.scrollByPageUp();
438 })("artext_page_up", "do pageup on article text");
441 if (mainPane
!is null) mainPane
.scrollByPageDown();
442 })("artext_page_down", "do pagedown on article text");
445 if (mainPane
!is null) mainPane
.scrollBy(-1);
446 })("artext_line_up", "do lineup on article text");
449 if (mainPane
!is null) mainPane
.scrollBy(1);
450 })("artext_line_down", "do linedown on article text");
452 // //////////////////////////////////////////////////////////////////// //
453 conRegFunc
!((bool forceOpera
=false) {
454 if (auto fldx
= getActiveFolder
) {
455 fldx
.withBaseReader((abase
, cur
, top
, alist
) {
456 if (cur
< alist
.length
) {
457 abase
.loadContent(alist
[cur
]);
458 if (auto art
= abase
[alist
[cur
]]) {
459 scope(exit
) art
.releaseContent();
460 auto path
= art
.getHeaderValue("path:");
461 //conwriteln("path: [", path, "]");
462 if (path
.startsWithCI("digitalmars.com!.POSTED.")) {
463 import std
.stdio
: File
;
465 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
466 string id
= art
.msgid
;
467 if (id
.length
> 2 && id
[0] == '<' && id
[$-1] == '>') id
= id
[1..$-1];
468 auto pid
= spawnProcess(
469 [getBrowserCommand(forceOpera
), "http://forum.dlang.org/post/"~id
],
470 File("/dev/zero"), File("/dev/null"), File("/dev/null"),
478 })("article_open_in_browser", "open the current article in browser (dlang forum)");
481 if (auto fldx
= getActiveFolder
) {
482 fldx
.withBaseReader((abase
, cur
, top
, alist
) {
483 if (cur
< alist
.length
) {
484 if (auto art
= abase
[alist
[cur
]]) {
485 auto path
= art
.getHeaderValue("path:");
486 if (path
.startsWithCI("digitalmars.com!.POSTED.")) {
487 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
488 string id
= art
.msgid
;
489 if (id
.length
> 2 && id
[0] == '<' && id
[$-1] == '>') id
= id
[1..$-1];
490 id
= "http://forum.dlang.org/post/"~id
;
491 setClipboardText(vbwin
, id
);
492 setPrimarySelection(vbwin
, id
);
498 })("article_copy_url_to_clipboard", "copy the url of the current article to clipboard (dlang forum)");
501 // //////////////////////////////////////////////////////////////////// //
502 conRegFunc
!((ConString oldmail
, ConString newmail
, ConString newname
) {
503 if (oldmail
.length
) {
504 if (newmail
.length
== 0) {
505 repmailreps
.remove(oldmail
.idup
);
508 mr
.oldmail
= oldmail
.idup
;
509 mr
.newmail
= newmail
.idup
;
510 mr
.newname
= newname
.idup
;
511 repmailreps
[mr
.oldmail
] = mr
;
514 })("append_replyto_mail_replacement", "append replacement for reply mails");
517 // //////////////////////////////////////////////////////////////////// //
519 if (auto fld = getActiveFolder
) {
521 if (mainPane
!is null) mainPane
.setupUnreadTimer();
524 })("article_prev", "go to previous article");
527 if (auto fld = getActiveFolder
) {
529 if (mainPane
!is null) mainPane
.setupUnreadTimer();
532 })("article_next", "go to next article");
535 if (auto fld = getActiveFolder
) {
537 if (mainPane
!is null) mainPane
.setupUnreadTimer();
540 })("article_pgup", "artiles list: page up");
543 if (auto fld = getActiveFolder
) {
545 if (mainPane
!is null) mainPane
.setupUnreadTimer();
548 })("article_pgdown", "artiles list: page down");
551 if (auto fld = getActiveFolder
) {
553 if (mainPane
!is null) mainPane
.setupUnreadTimer();
556 })("article_scroll_up", "scroll article list up");
559 if (auto fld = getActiveFolder
) {
561 if (mainPane
!is null) mainPane
.setupUnreadTimer();
564 })("article_scroll_down", "scroll article list up");
567 if (auto fld = getActiveFolder
) {
569 if (mainPane
!is null) mainPane
.setupUnreadTimer();
572 })("article_to_first", "go to first article");
575 if (auto fld = getActiveFolder
) {
577 if (mainPane
!is null) mainPane
.setupUnreadTimer();
580 })("article_to_last", "go to last article");
583 // //////////////////////////////////////////////////////////////////// //
585 if (auto fld = getActiveFolder
) {
586 auto postDg
= delegate (Account acc
) {
587 conwriteln("post with account '", acc
.name
, "' (", acc
.mail
, ")");
588 auto pw
= new PostWindow();
589 pw
.from
.str = acc
.realname
~" <"~acc
.mail
~">";
590 pw
.from
.readonly
= true;
591 if (auto nna
= cast(NntpAccount
)acc
) {
592 pw
.title
= "Reply to NNTP "~nna
.server
~":"~nna
.group
;
593 pw
.to
.str = nna
.group
;
594 pw
.to
.readonly
= true;
595 pw
.activeWidget
= pw
.subj
;
597 pw
.title
= "Reply from '"~acc
.name
~"' ("~acc
.mail
~")";
599 pw
.activeWidget
= pw
.to
;
606 auto acc
= fld.findAccountToPost();
608 auto wacc
= new SelectPopBoxWindow(defaultAcc
);
609 wacc
.onSelected
= postDg
;
615 conwriteln("post: no active folder");
617 })("new_post", "post a new article or message");
620 // //////////////////////////////////////////////////////////////////// //
621 static string
buildToStr (string name
, string mail
, ref string newfromname
) {
622 if (auto mrp
= mail
in repmailreps
) {
623 newfromname
= mrp
.newname
;
624 return mrp
.newname
~" <"~mrp
.newmail
~">";
626 return name
~" <"~mail
~">";
630 static void doReply (string repfld
) {
631 if (auto fld = getActiveFolder
) {
632 auto postDg
= delegate (Account acc
) {
633 conwriteln("reply with account '", acc
.name
, "' (", acc
.mail
, ")");
634 fld.withBaseReader((abase
, cur
, top
, alist
) {
635 if (cur
< alist
.length
) {
636 auto aidx
= alist
.ptr
[cur
];
637 abase
.loadContent(aidx
);
638 if (auto art
= abase
[aidx
]) {
639 assert(art
.contentLoaded
);
640 auto atext
= art
.getTextContent
;
641 auto pw
= new PostWindow();
642 pw
.from
.str = acc
.realname
~" <"~acc
.mail
~">";
643 pw
.from
.readonly
= true;
644 string from
= art
.fromname
;
646 if (auto nna
= cast(NntpAccount
)acc
) {
647 pw
.title
= "Reply to NNTP "~nna
.server
~":"~nna
.group
;
648 pw
.to
.str = nna
.group
;
649 pw
.to
.readonly
= true;
651 pw
.title
= "Reply from '"~acc
.name
~"' ("~acc
.mail
~")";
652 //pw.to.str = art.fromname~" <"~art.frommail~">";
654 if (repfld
!= "From") {
655 auto rf
= art
.getHeaderValue(repfld
);
657 //rs = buildToStr(art.fromname, repfld, from);
658 if (art
.getHeaderValue("List-Id").length
) {
661 rs
= art
.fromname
~" <"~rf
.idup
~">";
665 if (!rs
) rs
= buildToStr(art
.fromname
, art
.frommail
, from
);
668 pw
.subj
.str = "Re: "~art
.subj
;
671 auto vp
= from
.indexOf(" via Digitalmars-");
673 from
= from
[0..vp
].xstrip
;
674 if (from
.length
== 0) from
= "anonymous";
679 pw
.ed
.addText(" wrote:\n");
681 foreach (ConString s
; LineIterator
!false(atext
)) {
682 if (s
.length
== 0 || s
[0] == '>') pw
.ed
.addText(">"); else pw
.ed
.addText("> ");
688 pw
.replyto
= art
.msgid
;
689 pw
.references
= art
.getHeaderValue("References").xstrip
.idup
;
692 pw
.activeWidget
= pw
.ed
;
698 auto acc
= fld.findAccountToPost(fld.curidx
);
700 auto wacc
= new SelectPopBoxWindow(defaultAcc
);
701 wacc
.onSelected
= postDg
;
709 conRegFunc
!(() { doReply("Reply-To"); })("article_reply", "reply to the current article with \"reply-to\" field");
710 conRegFunc
!(() { doReply("From"); })("article_reply_to_from", "reply to the current article with \"from\" field");
713 // //////////////////////////////////////////////////////////////////// //
715 if (auto fld = getActiveFolder
) {
716 fld.withBaseReader((abase
, cur
, top
, alist
) {
717 if (cur
< alist
.length
) {
718 auto aidx
= alist
.ptr
[cur
];
719 abase
.loadContent(aidx
);
720 if (auto art
= abase
[aidx
]) {
721 assert(art
.contentLoaded
);
722 articleBogoMarkHam(art
);
723 //TODO: move out of spam
728 })("article_mark_ham", "mark current article as ham");
732 if (auto fld = getActiveFolder
) {
734 uint didx
= uint.max
;
735 Folder fldspam
= getSpamFolder
;
736 fld.withBaseReader((abase
, cur
, top
, alist
) {
737 if (cur
< alist
.length
) {
738 auto aidx
= alist
.ptr
[cur
];
739 abase
.loadContent(aidx
);
740 if (auto art
= abase
[aidx
]) {
741 assert(art
.contentLoaded
);
742 conwriteln("marking ", art
.msgid
, " as spam");
743 articleBogoMarkSpam(art
);
744 if (fld !is fldspam
) {
745 conwriteln(" should be moved to spam folder");
746 newart
= art
.clone();
753 if (didx
!= uint.max
) {
754 assert(newart
!is null);
755 conwriteln("removing ", newart
.msgid
, " from ", fld.folderPath
);
756 fld.withBaseWriter((abase
) {
757 abase
.softDeleted(didx
, true);
758 abase
.writeUpdates();
760 fld.markForRebuild();
761 fld.buildVisibleList();
764 if (newart
!is null && fldspam
!is null) {
765 newart
.unread
= false;
766 conwriteln("adding ", newart
.msgid
, " to ", fldspam
.folderPath
);
767 fldspam
.withBaseWriter((abase
) {
768 abase
.insert(newart
);
769 abase
.writeUpdates();
771 newart
.releaseContent();
772 fldspam
.markForRebuild();
773 fldspam
.buildVisibleList();
777 })("article_mark_spam", "mark current article as spam");
780 // //////////////////////////////////////////////////////////////////// //
781 conRegFunc
!((ConString fldname
) {
782 if (auto fld = getActiveFolder
) {
783 Folder destfld
= findFolderByPath(fldname
);
784 if (destfld
is null) { conwriteln("cannot find folder '", fldname
, "'"); }
785 if (destfld
is fld) { conwriteln("cannot move to the same folder"); return; }
787 uint didx
= uint.max
;
788 fld.withBaseReader((abase
, cur
, top
, alist
) {
789 if (cur
< alist
.length
) {
790 auto aidx
= alist
.ptr
[cur
];
791 abase
.loadContent(aidx
);
792 if (auto art
= abase
[aidx
]) {
793 assert(art
.contentLoaded
);
794 newart
= art
.clone();
799 if (didx
== uint.max || newart
is null) { conwriteln("article not found!"); return; }
801 assert(didx
!= uint.max
);
802 assert(newart
!is null);
803 conwriteln("removing ", newart
.msgid
, " from ", fld.folderPath
);
804 fld.withBaseWriter((abase
) {
805 abase
.softDeleted(didx
, true);
806 abase
.writeUpdates();
808 fld.markForRebuild();
809 fld.buildVisibleList();
811 conwriteln("adding ", newart
.msgid
, " to ", destfld
.folderPath
);
812 destfld
.withBaseWriter((abase
) {
813 abase
.insert(newart
);
814 abase
.writeUpdates();
816 newart
.releaseContent();
817 destfld
.markForRebuild();
818 destfld
.buildVisibleList();
821 })("article_move_to_folder", "move article to existing folder");
824 // //////////////////////////////////////////////////////////////////// //
826 if (auto fld = getActiveFolder
) {
828 if (mainPane
!is null) mainPane
.setupUnreadTimer();
831 })("article_to_parent", "jump to parent article, if any");
834 if (auto fld = getActiveFolder
) {
836 if (mainPane
!is null) mainPane
.setupUnreadTimer();
839 })("article_to_prev_sib", "jump to previous sibling");
842 if (auto fld = getActiveFolder
) {
844 if (mainPane
!is null) mainPane
.setupUnreadTimer();
847 })("article_to_next_sib", "jump to next sibling");
850 // //////////////////////////////////////////////////////////////////// //
852 if (auto fld = getActiveFolder
) {
853 if (auto acc
= findNntpAccountForFolder(fld)) {
854 fld.withBaseReader(delegate (abase
, cur
, top
, alist
) {
855 if (cur
< alist
.length
) {
856 if (auto art
= abase
[alist
[cur
]]) {
857 auto t
= fld.isTwittedNL(cur
);
858 auto setdg
= delegate (string name
, string mail
, string folder
, string title
) {
861 if (title
.length
!= 0) {
862 concmdfdg("twit_set name \"%s\" mail \"%s\" folder_mask \"%s\" title \"%s\"", (ConString cmd
) { xcmd
= cmd
.xstrip
.idup
; }, name
, mail
, folder
, title
);
865 if (title
.length
== 0) {
866 concmdfdg("twit_unset name \"%s\" mail \"%s\" folder_mask \"%s\"", (ConString cmd
) { xcmd
= cmd
.xstrip
.idup
; }, name
, mail
, folder
);
868 concmdfdg("twit_set name \"%s\" mail \"%s\" folder_mask \"%s\" title \"%s\"", (ConString cmd
) { xcmd
= cmd
.xstrip
.idup
; }, name
, mail
, folder
, title
);
872 import std
.path
: buildPath
;
873 auto fo
= VFile(buildPath(mailRootDir
, "auto_twits.rc"), "a");
875 } catch (Exception e
) {
876 conwriteln("ERROR writing twit info: ", e
.msg
);
880 auto tw
= new TitlerWindow(art
.fromname
, art
.frommail
, (acc
.inboxFolder
.startsWith("dmars_ng/") ?
"dmars_ng/*" : acc
.inboxFolder
), t
);
881 tw
.onSelected
= setdg
;
887 })("article_edit_poster_title", "edit poster's title of the current article");
890 // //////////////////////////////////////////////////////////////////// //
892 if (auto fld = getActiveFolder
) {
893 fld.withBaseReader((abase
, cur
, top
, alist
) {
894 if (cur
< alist
.length
) {
895 uint ridx
= alist
[cur
];
896 if (auto art
= abase
[ridx
]) {
897 while (abase
[ridx
].parent
!= 0) ridx
= abase
[ridx
].parent
;
898 twitThread(fld.folderPath
, art
.msgid
);
899 //TODO: mark thread articles as read
905 })("article_twit_thread", "twit current thread");
908 if (auto fld = getActiveFolder
) {
909 if (fld.toggleSoftDeleted()) {
910 if (fld.markAsRead()) {
911 if (mainPane
!is null) mainPane
.resetUnreadTimer();
916 })("article_softdelete_toggle", "toggle \"soft deleted\" flag on current article");
919 // //////////////////////////////////////////////////////////////////// //
921 if (auto fld = getActiveFolder
) {
922 if (fld.curidxValid
) {
923 fld.withBaseReader(delegate (abase
, cur
, top
, alist
) {
924 auto art
= abase
[alist
[cur
]];
926 conwriteln("============================");
927 abase
.loadContent(alist
[cur
]);
928 scope(exit
) art
.releaseContent();
930 foreach (ConString s
; art
.headersIterator
!false) {
932 if (from
.length
== 0 && s
.startsWithCI("From:")) from
= s
;
934 conwriteln("---------------");
935 conwriteln(" ", art
.fromname
, " <", art
.frommail
, ">");
936 conwriteln(" ", decodeq(from
));
939 foreach (char ch
; art
.fromname
) {
940 if (dc
.decode(cast(ubyte)ch
)) {
941 if (dc
.codepoint
> 127 || dc
.codepoint
< 32) conwritef
!" \\u%04X "(cast(uint)dc
.codepoint
);
942 else conwrite(cast(char)dc
.codepoint
);
950 })("article_dump_headers", "dump article headers");
953 if (auto fld = getActiveFolder
) {
957 })("folder_rebuild_index", "rebuild index file");
960 if (auto fld = getActiveFolder
) {
964 })("folder_pack_textdb", "pack (rebuild) text database for current folder");
966 conRegVar("folder_hide_old", "should old articles be hidden in UI?",
968 if (auto fld = getActiveFolder
) return fld.hideOldThreads
;
971 delegate (self
, bool nv
) {
972 if (auto fld = getActiveFolder
) fld.hideOldThreads
= nv
;
978 if (auto fld = getActiveFolder) {
979 fld.withBase(delegate (abase) {
980 uint idx = fld.curidx;
981 if (idx >= fld.length) {
983 } else if (auto art = abase[fld.baseidx(idx)]) {
984 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
985 idx = (idx+1)%fld.length;
988 foreach (immutable _; 0..fld.length) {
989 auto art = abase[fld.baseidx(idx)];
991 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
992 fld.curidx = cast(int)idx;
997 idx = (idx+1)%fld.length;
1001 })("find_mine", "find mine article");
1004 // //////////////////////////////////////////////////////////////////// //
1005 conRegVar
!dbg_dump_keynames("dbg_dump_key_names", "dump key names");
1008 // //////////////////////////////////////////////////////////////////// //
1009 conRegFunc
!((uint idx
, bool runImageViewer
=false, ConString fname
=null) {
1010 import std
.format
: format
;
1011 if (auto fld = getActiveFolder
) {
1012 fld.withBaseReader(delegate (abase
, cur
, top
, alist
) {
1013 if (cur
>= alist
.length
) return;
1014 if (auto art
= abase
[alist
[cur
]]) {
1015 abase
.loadContent(alist
[cur
]);
1016 scope(exit
) art
.releaseContent();
1017 string attfname
= null;
1019 art
.writeAttachmentToFile(idx
, delegate (ConString attname
) {
1020 //import std.format : format;
1021 import std
.file
: isDir
;
1022 //conwriteln("attnamefix: attname=<", attname, ">; fname=<", fname, ">");
1023 if (fname
.length
== 0) {
1024 attfname
= "/tmp/"~Article
.fixAttachmentName(attname
);
1025 //conwriteln("attfname(0): <", attfname, ">");
1029 string res
= fname
.idup
;
1030 if (res
.length
&& res
[$-1] != '/') res
~= '/';
1031 res
~= Article
.fixAttachmentName(attname
);
1033 //conwriteln("attfname(1): <", attfname, ">");
1036 attfname
= fname
.idup
;
1037 //conwriteln("attfname(2): <", attfname, ">");
1040 } catch (Exception e
) {
1041 conwriteln("ERROR writing attachment (", e
.msg
, ")");
1044 if (runImageViewer
&& attfname
.length
) {
1047 //FIXME: make regvar for image viewer
1048 //auto pid = execute(["keh", attfname], null, Config.detached);
1050 import std
.stdio
: File
;
1051 auto frd
= File("/dev/null");
1052 auto fwr
= File("/dev/null", "w");
1053 spawnProcess(["keh", attfname
], frd
, fwr
, fwr
, null, Config
.detached
);
1054 } catch (Exception e
) {
1055 conwriteln("ERROR executing image viewer (", e
.msg
, ")");
1061 { import core
.memory
: GC
; GC
.collect(); GC
.minimize(); }
1062 })("attach_save", "save attach: attach_save index [filename]");
1066 // ////////////////////////////////////////////////////////////////////////// //
1067 //FIXME: turn into property
1068 final class ArticleTextScrollEvent
{}
1069 __gshared ArticleTextScrollEvent evArticleScroll
;
1070 shared static this () {
1071 evArticleScroll
= new ArticleTextScrollEvent();
1074 void postArticleScroll () {
1075 if (vbwin
!is null && !vbwin
.eventQueued
!ArticleTextScrollEvent
) vbwin
.postTimeout(evArticleScroll
, 25);
1079 class MarkAsUnreadEvent
{
1084 __gshared
int unreadTimeoutInterval
= 600;
1085 __gshared
int scrollKeepLines
= 3;
1088 void postMarkAsUnreadEvent () {
1089 if (unreadTimeoutInterval
> 0 && unreadTimeoutInterval
< 10000 && vbwin
!is null) {
1090 if (auto fld = getActiveFolder
) {
1091 if (fld.curidxValid
&& fld.curidxUnread
) {
1092 //conwriteln("setting new unread timer");
1093 auto evt
= new MarkAsUnreadEvent();
1095 evt
.idx
= fld.curidx
;
1096 vbwin
.postTimeout(evt
, unreadTimeoutInterval
);
1103 __gshared MainPaneWindow mainPane
;
1106 final class MainPaneWindow
: SubWindow
{
1107 string
[] emlines
; // in utf
1108 uint emlinesBeforeAttaches
;
1109 string lastDecodedMsgFolderPath
;
1110 string lastDecodedMsgId
;
1111 int articleTextTopLine
= 0;
1112 int articleDestTextTopLine
= 0;
1113 //__gshared Timer unreadTimer; // as main pane is never destroyed, there's no need to kill the timer
1114 //__gshared Folder unreadFolder;
1115 //__gshared uint unreadIdx;
1119 super(null, 0, 0, VBufWidth
, VBufHeight
);
1120 mType
= Type
.OnBottom
;
1124 // //////////////////////////////////////////////////////////////////// //
1125 static struct WebLink
{
1127 int x
, len
; // in pixels
1129 string text
; // visual text
1131 bool nofirst
= false;
1133 @property bool isAttach () const pure nothrow @safe @nogc { return (attachnum
>= 0); }
1137 int lastUrlIndex
= -1;
1139 void clearDecodedText () {
1140 emurls
[] = WebLink
.init
;
1143 emurls
.assumeSafeAppend
;
1145 emlines
.assumeSafeAppend
;
1146 lastDecodedMsgFolderPath
= null;
1147 lastDecodedMsgId
= null;
1148 articleTextTopLine
= 0;
1149 articleDestTextTopLine
= 0;
1154 int findUrlIndexAt (int mx
, int my
) {
1155 int tpX0
= guiGroupListWidth
+2+1;
1156 int tpX1
= VBufWidth
-1-guiMessageTextLPad
*2-guiScrollbarWidth
;
1157 int tpY0
= guiThreadListHeight
+1;
1158 int tpY1
= VBufHeight
-1;
1160 int y
= tpY0
+linesInHeader
*gxTextHeightUtf
+2+1+guiMessageTextVPad
;
1162 if (mx
< tpX0 || mx
> tpX1
) return -1;
1163 if (my
< y || my
> tpY1
) return -1;
1167 // yeah, i can easily calculate this, i know
1168 uint idx
= articleTextTopLine
;
1169 while (idx
< emlines
.length
&& y
< VBufHeight
) {
1170 if (my
>= y
&& my
< y
+gxTextHeightUtf
) {
1171 foreach (immutable uidx
, const ref WebLink wl
; emurls
) {
1172 //conwriteln("checking url [#", uidx, "]; idx=", idx, "; ly=", wl.ly);
1174 if (mx
>= wl
.x
&& mx
< wl
.x
+wl
.len
) return cast(int)uidx
;
1179 y
+= gxTextHeightUtf
+guiMessageTextInterline
;
1185 WebLink
* findUrlAt (int mx
, int my
) {
1186 auto uidx
= findUrlIndexAt(mx
, my
);
1187 return (uidx
>= 0 ?
&emurls
[uidx
] : null);
1190 private void emlDetectUrls (uint textlines
) {
1191 static immutable string
[3] protos
= [ "https", "http", "ftp" ];
1195 static sptrdiff
urlepos (const(char)[] s
, sptrdiff spos
) {
1196 assert(spos
< s
.length
);
1197 spos
= s
.indexOf("://", spos
);
1201 while (spos
< s
.length
) {
1203 if (ch
== '/') break;
1204 if (ch
<= ' ') return spos
;
1205 if (ch
!= '.' && ch
!= '-' && !ch
.isalnum
) return spos
;
1208 if (spos
>= s
.length
) return cast(sptrdiff
)s
.length
;
1210 assert(s
[spos
] == '/');
1213 bool wasSharp
= false;
1214 for (; spos
< s
.length
; ++spos
) {
1215 import iv
.strex
: isalnum
;
1217 if (ch
<= ' ' || ch
== '<' || ch
== '>' || ch
== '"' || ch
== '\'' || ch
>= 127) return spos
;
1220 if (wasSharp
) return spos
;
1231 if (ch
== '(' || ch
== '{' || ch
== '[') {
1232 if (brlevel
>= brcmap
.length
) return spos
; // too nested
1233 if (s
.length
-spos
< 2) return spos
; // no more chars, ignore
1234 if (!isalnum(s
[spos
+1]) && s
[spos
+1] != '_') return spos
; // ignore
1235 // looks like URL part
1237 case '(': ch
= ')'; break;
1238 case '[': ch
= ']'; break;
1239 case '{': ch
= '}'; break;
1241 brcmap
[brlevel
++] = ch
;
1245 if (ch
== ')' || ch
== '}' || ch
== ']') {
1246 if (brlevel
== 0 || ch
!= brcmap
[brlevel
-1]) return spos
;
1250 // other punctuation
1251 if (brlevel
== 0 && !isalnum(ch
)) {
1252 // other special chars
1253 if (s
.length
-spos
< 2) return spos
; // no more chars, ignore
1254 if (!isalnum(s
[spos
+1]) && s
[spos
+1] != '_') {
1255 if (ch
== '.' || ch
== '!' || ch
== ';' || ch
== ',' || s
[spos
+1] != ch
) return spos
; // ignore
1260 if (spos
>= s
.length
) spos
= cast(sptrdiff
)s
.length
;
1264 if (textlines
> emlines
.length
) textlines
= cast(uint)emlines
.length
; // just in case
1265 foreach (immutable cy
, string s
; emlines
[0..textlines
]) {
1266 if (s
.length
== 0) continue;
1267 auto pos
= s
.indexOf("://");
1271 foreach (string proto
; protos
) {
1272 if (spos
>= proto
.length
&& proto
.strEquCI(s
[spos
-proto
.length
..spos
])) {
1273 if (spos
== proto
.length ||
!s
[spos
-proto
.length
-1].isalnum
) {
1275 spos
-= proto
.length
;
1282 auto epos
= urlepos(s
, spos
);
1284 wl
.nofirst
= (spos
> 0);
1285 wl
.ly
= cast(int)cy
;
1286 auto kr
= GxKerning(4);
1287 s
[0..epos
].utfByDCharSPos(delegate (dchar ch
, usize stpos
) {
1288 int w
= kr
.fixWidthPre(ch
);
1289 if (stpos
== spos
) wl
.x
= w
;
1291 wl
.len
= kr
.finalWidth
-wl
.x
;
1292 wl
.url
= wl
.text
= s
[spos
..epos
];
1298 pos
= s
.indexOf("://", pos
);
1302 int attachcount
= 0;
1303 foreach (immutable uint cy
; textlines
..cast(uint)emlines
.length
) {
1304 string s
= emlines
[cy
];
1305 if (s
.length
== 0) continue;
1306 auto spos
= s
.indexOf("attach:");
1307 if (spos
< 0) continue;
1309 while (epos
< s
.length
&& s
.ptr
[epos
] > ' ') ++epos
;
1310 //if (attachcount >= parts.length) break;
1312 wl
.nofirst
= (spos
> 0);
1313 wl
.ly
= cast(int)cy
;
1314 //wl.x = gxTextWidthUtf(s[0..spos]);
1315 //wl.len = gxTextWidthUtf(s[spos..epos]);
1316 auto kr
= GxKerning(4);
1317 s
[0..epos
].utfByDCharSPos(delegate (dchar ch
, usize stpos
) {
1318 int w
= kr
.fixWidthPre(ch
);
1319 if (stpos
== spos
) wl
.x
= w
;
1321 wl
.len
= kr
.finalWidth
-wl
.x
;
1322 wl
.url
= wl
.text
= s
[spos
..epos
];
1323 //if (spos > 0) ++wl.x; // this makes text bolder, no need to
1324 wl
.attachnum
= attachcount
;
1325 //wl.attachfname = s[spos+7..epos];
1326 //wl.part = parts[attachcount];
1332 bool needToDecodeArticleTextNL (Folder
fld, Article art
) nothrow @trusted @nogc {
1333 if (art
is null) return false;
1334 if (lastDecodedMsgId
!= art
.msgid
) return true;
1335 return (fld !is null ? lastDecodedMsgFolderPath
!= fld.folderPath
: lastDecodedMsgFolderPath
.length
!= 0);
1338 // fld is locked here
1339 void decodeArticleTextNL (Folder
fld, Article art
) {
1340 if (art
is null) { clearDecodedText(); return; }
1341 if (!needToDecodeArticleTextNL(fld, art
)) return;
1344 emlines
~= null; // hack; this dummy line will be removed
1345 lastDecodedMsgFolderPath
= (fld !is null ?
fld.folderPath
: null);
1346 lastDecodedMsgId
= art
.msgid
;
1348 bool lastEndsWithSpace () { return (emlines
[$-1].length ? emlines
[$-1][$-1] == ' ' : false); }
1352 static string
addQuotes(T
:const(char)[]) (T s
, int qlevel
) {
1353 enum QuoteStr
= ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ";
1355 static if (is(T
== string
) ||
is (T
== typeof(null))) return s
; else return s
.idup
;
1357 if (qlevel
> QuoteStr
.length
-1) qlevel
= cast(int)QuoteStr
.length
-1;
1358 return QuoteStr
[$-qlevel
-1..$]~s
;
1361 // returns quote level
1362 static int removeQuoting (ref ConString s
) {
1363 // calculate quote level
1365 if (s
.length
&& s
[0] == '>') {
1366 usize lastqpos
= 0, pos
= 0;
1367 while (pos
< s
.length
) {
1368 if (s
.ptr
[pos
] != '>') {
1369 if (s
.ptr
[pos
] != ' ') break;
1376 if (s
.length
-lastqpos
> 1 && s
.ptr
[lastqpos
+1] == ' ') ++lastqpos
;
1383 bool inCode
= false;
1385 void putLine (ConString s
) {
1386 int qlevel
= removeQuoting(s
);
1387 //conwriteln("qlevel=", qlevel, "; s=[", s, "]");
1388 // empty line: just insert it
1389 if (s
.length
== 0) {
1390 emlines
~= addQuotes(null, qlevel
).xstripright
;
1392 // join code lines if it is possible
1393 if (inCode
&& qlevel
== lastQLevel
&& lastEndsWithSpace
) {
1394 //conwriteln("<", s, ">");
1395 emlines
[$-1] ~= addQuotes(s
.xstrip
, qlevel
);
1398 // two spaces at the beginning usually means "this is code"; don't wrap it
1399 if (s
.length
>= 1 && s
[0] == '\t') {
1400 emlines
~= addQuotes(s
, qlevel
);
1401 // join next lines if it is possible
1403 //conwriteln("[", s, "]");
1404 lastQLevel
= qlevel
;
1409 bool newline
= false;
1410 if (lastQLevel
!= qlevel ||
!lastEndsWithSpace
) {
1414 //while (s.length > 1 && s.ptr[0] <= ' ') s = s[1..$];
1419 while (epos
< s
.length
&& s
.ptr
[epos
] <= ' ') ++epos
;
1421 //assert(s[0] > ' ');
1422 while (epos
< s
.length
&& s
.ptr
[epos
] <= ' ') ++epos
;
1424 while (epos
< s
.length
&& s
.ptr
[epos
] > ' ') ++epos
;
1426 while (epos
< s
.length
&& s
.ptr
[epos
] <= ' ') ++epos
;
1427 if (!newline
&& emlines
[$-1].length
+xlen
<= 80) {
1428 // no wrapping, continue last line
1429 emlines
[$-1] ~= s
[0..epos
];
1432 // wrapping; new line
1433 emlines
~= addQuotes(s
[0..epos
], qlevel
);
1437 if (newline
) emlines
~= addQuotes(null, qlevel
);
1439 lastQLevel
= qlevel
;
1443 foreach (ConString s
; LineIterator
!false(art
.getTextContent
)) putLine(s
);
1444 // remove first dummy line
1445 if (emlines
.length
) emlines
= emlines
[1..$];
1446 // remove trailing empty lines
1447 while (emlines
.length
&& emlines
[$-1].xstrip
.length
== 0) emlines
.length
-= 1;
1448 } catch (Exception e
) {
1449 conwriteln("================================= ERROR: ", e
.msg
, " =================================");
1450 conwriteln(e
.toString
);
1454 auto lcount
= cast(uint)emlines
.length
;
1455 emlinesBeforeAttaches
= lcount
;
1458 art
.forEachAttachment(delegate(ConString type
, ConString filename
) {
1459 if (attcount
== 0) { emlines
~= null; emlines
~= null; }
1460 import std
.format
: format
;
1461 if (type
.length
== 0) type
= "unknown/unknown";
1462 string s
= " [%02s] attach:%s -- %s".format(attcount
, Article
.fixAttachmentName(filename
), type
);
1467 emlDetectUrls(lcount
);
1470 @property int visibleArticleLines () {
1471 int y
= guiThreadListHeight
+1+linesInHeader
*gxTextHeightUtf
+2+guiMessageTextVPad
;
1472 return (VBufHeight
-y
)/(gxTextHeightUtf
+guiMessageTextInterline
);
1475 void normalizeArticleTopLine () {
1476 int lines
= visibleArticleLines
;
1477 if (lines
< 1 || emlines
.length
<= lines
) {
1478 articleTextTopLine
= 0;
1479 articleDestTextTopLine
= 0;
1481 if (articleTextTopLine
< 0) articleTextTopLine
= 0;
1482 if (articleTextTopLine
+lines
> emlines
.length
) {
1483 articleTextTopLine
= cast(int)emlines
.length
-lines
;
1484 if (articleTextTopLine
< 0) articleTextTopLine
= 0;
1489 void doScrollStep () {
1490 auto oldtop
= articleTextTopLine
;
1491 foreach (immutable _
; 0..6) {
1492 normalizeArticleTopLine();
1493 if (articleDestTextTopLine
< articleTextTopLine
) {
1494 --articleTextTopLine
;
1495 } else if (articleDestTextTopLine
> articleTextTopLine
) {
1496 ++articleTextTopLine
;
1500 normalizeArticleTopLine();
1502 if (articleTextTopLine
== oldtop
) {
1503 // can't scroll anymore
1504 articleDestTextTopLine
= articleTextTopLine
;
1507 postScreenRebuild();
1508 postArticleScroll();
1511 void scrollBy (int delta
) {
1512 articleDestTextTopLine
+= delta
;
1516 void scrollByPageUp () {
1517 int lines
= visibleArticleLines
-scrollKeepLines
;
1518 if (lines
< 1) lines
= 1;
1522 void scrollByPageDown () {
1523 int lines
= visibleArticleLines
-scrollKeepLines
;
1524 if (lines
< 1) lines
= 1;
1528 void resetUnreadTimer () {
1530 if (unreadTimer !is null) { unreadTimer.destroy(); unreadTimer = null; }
1531 unreadFolder = null;
1532 unreadIdx = unreadIdx.max;
1536 void setupUnreadTimer () {
1537 postMarkAsUnreadEvent();
1539 if (auto fld = getActiveFolder) {
1540 if (fld.curidxValid && fld.curidxUnread) {
1541 conwriteln("setting new unread timer");
1544 unreadIdx = fld.curidx;
1545 unreadTimer = new Timer(2000, delegate () {
1546 conwriteln("unread timer fired");
1547 if (unreadFolder is getActiveFolder && unreadIdx == unreadFolder.curidx) {
1548 conwriteln("*** unread timer hit!");
1549 unreadFolder.curidxUnread = false;
1553 //fld.curidxUnread = false;
1560 //enum gxRGBA = (clampToByte(a)<<24)|(clampToByte(r)<<16)|(clampToByte(g)<<8)|clampToByte(b);
1561 uint groupListBackColor
= gxRGB
!(30, 30, 30);
1562 uint groupListDivLineColor
= gxRGB
!(255, 255, 255);
1564 uint threadListBackColor
= gxRGB
!(30, 30, 30);
1565 uint threadListDivLineColor
= gxRGB
!(255, 255, 255);
1567 uint messageHeaderBackColor
= gxRGB
!(20, 20, 20);
1568 uint messageHeaderFromColor
= gxRGB
!(0, 128, 128);
1569 uint messageHeaderToColor
= gxRGB
!(0, 128, 128);
1570 uint messageHeaderSubjColor
= gxRGB
!(0, 128, 128);
1571 uint messageHeaderDateColor
= gxRGB
!(0, 128, 128);
1572 uint messageHeaderDivLineColor
= gxRGB
!(180, 180, 180);
1574 uint messageTextBackColor
= gxRGB
!(37, 37, 37);
1575 uint messageTextNormalColor
= gxRGB
!(128+46, 128+46, 128+46);
1576 uint messageTextQuote0Color
= gxRGB
!(128, 128, 0);
1577 uint messageTextQuote1Color
= gxRGB
!(0, 128, 128);
1578 uint messageTextLinkColor
= gxRGB
!(0, 200, 200);
1579 uint messageTextLinkHoverColor
= gxRGB
!(0, 255, 255);
1580 uint messageTextLinkPressedColor
= gxRGB
!(255, 0, 255);
1582 uint twithShadeColor
= gxRGBA
!(0, 0, 80, 127);
1583 uint twithTextColor
= gxRGB
!(255, 0, 0);
1584 uint twithTextOutlineColor
= gxRGB
!(0, 0, 0);
1586 // //////////////////////////////////////////////////////////////////// //
1587 //TODO: move parts to widgets
1588 override void onPaint () {
1591 gxFillRect(0, 0, guiGroupListWidth
, VBufHeight
, groupListBackColor
);
1592 gxVLine(guiGroupListWidth
, 0, VBufHeight
, groupListDivLineColor
);
1594 gxFillRect(guiGroupListWidth
+1, 0, VBufWidth
, guiThreadListHeight
, threadListBackColor
);
1595 gxHLine(guiGroupListWidth
+1, guiThreadListHeight
, VBufWidth
, threadListDivLineColor
);
1597 // called with locked folder
1598 void drawArticle (ArticleBase abase
, Folder
fld, Article art
, uint cidx
, uint aidx
) {
1599 import core
.stdc
.stdio
: snprintf
;
1600 import std
.format
: format
;
1601 import std
.datetime
;
1603 const(char)[] tbufs
;
1605 void xfmt (string s
, const(char)[][] strs
...) {
1607 void puts (const(char)[] s
...) {
1608 foreach (char ch
; s
) {
1609 if (dpos
>= tbuf
.length
) break;
1614 if (strs
.length
&& s
.length
> 1 && s
[0] == '%' && s
[1] == 's') {
1623 tbufs
= tbuf
[0..dpos
];
1626 if (needToDecodeArticleTextNL(fld, art
)) {
1627 abase
.loadContent(aidx
);
1628 scope(exit
) abase
.releaseContent(aidx
);
1629 assert(art
.contentLoaded
);
1630 decodeArticleTextNL(fld, art
);
1634 gxClipX0 = guiGroupListWidth+2;
1635 gxClipX1 = VBufWidth-1;
1636 gxClipY0 = guiThreadListHeight+1;
1637 gxClipY1 = VBufHeight-1;
1639 gxClipRect
= GxRect(GxPoint(guiGroupListWidth
+2, guiThreadListHeight
+1), GxPoint(VBufWidth
-1, VBufHeight
-1));
1641 int msx
= lastMouseX
;
1642 int msy
= lastMouseY
;
1644 int curDrawYMul
= 1;
1647 immutable int hdrHeight
= (3+(art
.toname
.length || art
.tomail
.length ?
1 : 0))*gxTextHeightUtf
+2;
1648 gxFillRect(gxClipRect
.x0
, gxClipRect
.y0
, gxClipRect
.x1
-gxClipRect
.x0
+1, hdrHeight
, messageHeaderBackColor
);
1650 xfmt("From: %s <%s>", art
.fromname
, art
.frommail
);
1651 gxDrawTextUtf(gxClipRect
.x0
+guiMessageTextLPad
, gxClipRect
.y0
+0*gxTextHeightUtf
+1, tbufs
, messageHeaderFromColor
);
1652 if (art
.toname
.length || art
.tomail
.length
) {
1653 xfmt("To: %s <%s>", art
.toname
, art
.tomail
);
1654 gxDrawTextUtf(gxClipRect
.x0
+guiMessageTextLPad
, gxClipRect
.y0
+curDrawYMul
*gxTextHeightUtf
+1, tbufs
, messageHeaderToColor
);
1657 xfmt("Subject: %s", art
.subj
);
1658 gxDrawTextUtf(gxClipRect
.x0
+guiMessageTextLPad
, gxClipRect
.y0
+curDrawYMul
*gxTextHeightUtf
+1, tbufs
, messageHeaderSubjColor
);
1661 auto t
= SysTime
.fromUnixTime(art
.time
);
1662 //string s = "Date: %04d/%02d/%02d %02d:%02d:%02d".format(t.year, t.month, t.day, t.hour, t.minute, t.second);
1663 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
);
1664 gxDrawTextUtf(gxClipRect
.x0
+guiMessageTextLPad
, gxClipRect
.y0
+curDrawYMul
*gxTextHeightUtf
+1, tbuf
[0..tlen
], messageHeaderDateColor
);
1670 linesInHeader
= curDrawYMul
;
1671 int y
= gxClipRect
.y0
+curDrawYMul
*gxTextHeightUtf
+2;
1673 gxHLine(gxClipRect
.x0
, y
, gxClipRect
.x1
-gxClipRect
.x0
+1, messageHeaderDivLineColor
);
1675 gxFillRect(gxClipRect
.x0
, y
, gxClipRect
.x1
-gxClipRect
.x0
+1, VBufHeight
-y
, messageTextBackColor
);
1676 y
+= guiMessageTextVPad
;
1680 normalizeArticleTopLine();
1682 bool drawUpMark
= (articleTextTopLine
> 0);
1683 bool drawDownMark
= false;
1685 uint idx
= articleTextTopLine
;
1686 bool msvisible
= isMouseVisible
;
1687 while (idx
< emlines
.length
&& y
< VBufHeight
) {
1689 string s
= emlines
[idx
];
1691 foreach (char ch
; s
) {
1692 if (ch
<= ' ') continue;
1693 if (ch
!= '>') break;
1697 uint clr
= messageTextNormalColor
;
1699 final switch (qlevel
%2) {
1700 case 0: clr
= messageTextQuote0Color
; break;
1701 case 1: clr
= messageTextQuote1Color
; break;
1705 gxDrawTextUtf(GxDrawTextOptions
.TabColor(4, clr
), gxClipRect
.x0
+guiMessageTextLPad
, y
, s
);
1707 foreach (const ref WebLink wl
; emurls
) {
1709 uint lclr
= messageTextLinkColor
;
1710 if (msvisible
&& msy
>= y
&& msy
< y
+gxTextHeightUtf
&&
1711 msx
>= gxClipRect
.x0
+1+guiMessageTextLPad
+wl
.x
&&
1712 msx
< gxClipRect
.x0
+1+guiMessageTextLPad
+wl
.x
+wl
.len
)
1714 lclr
= (lastMouseLeft ? messageTextLinkPressedColor
: messageTextLinkHoverColor
);
1716 gxDrawTextUtf(GxDrawTextOptions
.TabColorFirstFull(4, lclr
, wl
.nofirst
), gxClipRect
.x0
+guiMessageTextLPad
+wl
.x
, y
, wl
.text
);
1720 if (gxClipRect
.y1
-y
< gxTextHeightUtf
&& emlines
.length
-idx
> 0) drawDownMark
= true;
1723 y
+= gxTextHeightUtf
+guiMessageTextInterline
;
1726 //gxDrawTextOutScaledUtf(1, gxClipRect.x1-gxTextWidthUtf(triangleDownStr)-3, sty, triangleDownStr, (drawUpMark ? gxRGB!(255, 255, 255) : gxRGB!(85, 85, 85)), gxRGB!(0, 69, 69));
1727 //gxDrawTextOutScaledUtf(1, gxClipRect.x1-gxTextWidthUtf(triangleUpStr)-3, gxClipRect.y1-7, triangleUpStr, (drawDownMark ? gxRGB!(255, 255, 255) : gxRGB!(85, 85, 85)), gxRGB!(0, 69, 69));
1729 gxDrawScrollBar(GxRect(gxClipRect
.x1
-10, sty
+10, 5, gxClipRect
.y1
-sty
-17), cast(int)emlines
.length
-1, idx
-1);
1731 string twittext
= fld.isTwittedNL(cidx
);
1732 if (twittext
!is null) {
1733 foreach (immutable dy
; gxClipRect
.y0
+3*gxTextHeightUtf
+2..gxClipRect
.y1
+1) {
1734 foreach (immutable dx
; gxClipRect
.x0
..gxClipRect
.x1
+1) {
1735 if ((dx^dy
)&1) gxPutPixel(dx
, dy
, twithShadeColor
);
1739 if (twittext
.length
) {
1740 int tx
= gxClipRect
.x0
+(gxClipRect
.width
-gxTextWidthScaledUtf(2, twittext
))/2-1;
1741 int ty
= gxClipRect
.y0
+(gxClipRect
.height
-3*gxTextHeightUtf
)/2-1;
1742 gxDrawTextOutScaledUtf(2, tx
, ty
, twittext
, twithTextColor
, twithTextOutlineColor
);
1747 void drawThreadList (Folder
fld) {
1748 if (fld.needRebuild
) fld.buildVisibleList();
1749 fld.makeCurrentVisible();
1750 fld.withBaseReader(delegate (abase
, cur
, top
, alist
) {
1751 if (alist
.length
== 0) return;
1753 gxClipRect
.x0
= guiGroupListWidth
+2;
1754 gxClipRect
.x1
= VBufWidth
-1-5;
1756 gxClipRect
.y1
= guiThreadListHeight
-1;
1757 immutable uint origX0
= gxClipRect
.x0
;
1758 immutable uint origX1
= gxClipRect
.x1
;
1759 immutable uint origY0
= gxClipRect
.y0
;
1760 immutable uint origY1
= gxClipRect
.y1
;
1762 //conwriteln(fld.msgtop, " : ", fld.list.length);
1764 while (idx
< alist
.length
&& y
< guiThreadListHeight
) {
1765 import std
.format
: format
;
1766 import std
.datetime
;
1768 if (y
>= guiThreadListHeight
) break;
1769 if (idx
>= alist
.length
) break;
1771 gxClipRect
.x0
= origX0
;
1772 gxClipRect
.x1
= origX1
;
1774 auto art
= abase
[alist
[idx
]];
1776 //conwriteln(idx, " : ", fld.list.length);
1778 uint cc
= gxRGB
!(0, 127, 127);
1779 if (art
.softDeleted
) cc
= gxRGB
!(0, 127-30, 127-30);
1780 gxFillRect(gxClipRect
.x0
, y
, gxClipRect
.width
-1, gxTextHeightUtf
, cc
);
1782 gxClipRect
.x0
= gxClipRect
.x0
+1;
1783 gxClipRect
.x1
= gxClipRect
.x1
-1;
1785 //uint clr = (idx != fld.curidx ? gxRGB!(255, 127, 0) : gxRGB!(255, 255, 255));
1786 uint clr
= (art
.unread ? gxRGB
!(255, 255, 255) : gxRGB
!(255-60, 127-60, 0));
1787 uint clr1
= (art
.unread ? gxRGB
!(255, 255, 0) : gxRGB
!(255-60-40, 127-60-40, 0));
1789 if (art
.depth
!= 0 && !art
.unread
) {
1790 clr
= gxRGB
!(255-90, 127-90, 0);
1791 clr1
= gxRGB
!(255-90-40, 127-90-40, 0);
1794 if (!art
.unread
&& !art
.softDeleted
) {
1795 if (fld.isTwittedNL(idx
) !is null) { clr
= gxRGB
!(60, 0, 0); clr1
= gxRGB
!(60, 0, 0); }
1796 if (fld.isHighlightedNL(idx
)) { clr
= gxRGB
!(0, 190, 0); clr1
= gxRGB
!(0, 190-40, 0); }
1797 } else if (art
.softDeleted
) {
1798 //clr = gxRGB!(255-80, 127-80, 0);
1799 //clr1 = gxRGB!(255-80-40, 127-80-40, 0);
1800 clr
= clr1
= gxRGB
!(127, 0, 0);
1803 auto t
= SysTime
.fromUnixTime(art
.time
);
1804 //string s = "%04d/%02d/%02d %02d:%02d".format(t.year, t.month, t.day, t.hour, t.minute);
1807 import core
.stdc
.stdio
: snprintf
;
1809 //string s = "%02d/%02d %02d:%02d".format(t.month, t.day, t.hour, t.minute);
1810 auto len
= snprintf(tmpbuf
.ptr
, tmpbuf
.length
, "%02d/%02d/%02d %02d:%02d", t
.year
%100, t
.month
, t
.day
, t
.hour
, t
.minute
);
1811 timewdt
= gxDrawTextUtf(gxClipRect
.x1
-gxTextWidthUtf(tmpbuf
[0..len
]), y
, tmpbuf
[0..len
], clr
);
1813 if (timewdt
%8) timewdt
= (timewdt|
7)+1;
1815 string from
= art
.fromname
;
1817 auto vp
= from
.indexOf(" via Digitalmars-");
1819 from
= from
[0..vp
].xstrip
;
1820 if (from
.length
== 0) from
= "anonymous";
1824 gxClipRect
.x1
= gxClipRect
.x1
-/*(13*6+4)*2+33*/timewdt
;
1825 enum FromWidth
= 22*6*2+88;
1826 gxDrawTextUtf(gxClipRect
.x1
-FromWidth
, y
, from
, clr
);
1827 gxDrawTextUtf(gxClipRect
.x1
-FromWidth
+gxTextWidthUtf(from
)+4, y
, "<", clr1
);
1828 gxDrawTextUtf(gxClipRect
.x1
-FromWidth
+gxTextWidthUtf(from
)+4+gxTextWidthUtf("<")+1, y
, art
.frommail
, clr1
);
1829 gxDrawTextUtf(gxClipRect
.x1
-FromWidth
+gxTextWidthUtf(from
)+4+gxTextWidthUtf("<")+1+gxTextWidthUtf(art
.frommail
)+1, y
, ">", clr1
);
1831 gxClipRect
.x1
= gxClipRect
.x1
-FromWidth
;
1832 gxDrawTextUtf(gxClipRect
.x0
+art
.depth
*3, y
, art
.subj
, clr
);
1833 foreach (immutable dx
; 0..art
.depth
) gxPutPixel(gxClipRect
.x0
+1+dx
*3, y
+gxTextHeightUtf
/2, gxRGB
!(70, 70, 70));
1835 if (art
.softDeleted
) {
1836 gxClipRect
.x0
= origX0
;
1837 gxClipRect
.x1
= origX1
;
1838 gxHLine(gxClipRect
.x0
, y
+gxTextHeightUtf
/2, gxClipRect
.x1
-gxClipRect
.x0
+1, clr
);
1842 y
+= gxTextHeightUtf
;
1847 gxClipRect
.x0
= origX0
;
1848 gxClipRect
.x1
= origX1
+5;
1849 gxClipRect
.y0
= origY0
;
1850 gxClipRect
.y1
= origY1
;
1851 gxDrawScrollBar(GxRect(gxClipRect
.x1
-5, gxClipRect
.y0
, 4, gxClipRect
.height
-1), cast(int)alist
.length
-1, idx
-1);
1854 if (cur
< alist
.length
) drawArticle(abase
, fld, abase
[alist
[cur
]], cur
, alist
[cur
]);
1859 folderMakeCurVisible();
1862 foreach (immutable idx
, Folder
fld; folders
) {
1863 if (idx
< folderTop
) continue;
1864 if (ofsy
>= VBufHeight
) break;
1866 if (idx
== folderCur
) gxFillRect(0, ofsy
-1, guiGroupListWidth
, (gxTextHeightUtf
+2), gxRGB
!(0, 127, 127));
1867 gxClipRect
.x0
= ofsx
-1;
1868 gxClipRect
.y0
= ofsy
;
1869 gxClipRect
.x1
= guiGroupListWidth
-3;
1870 int depth
= folderDepth(idx
);
1871 uint clr
= gxRGB
!(255-30, 127-30, 0);
1872 if (fld.unreadCount
) {
1873 clr
= gxRGB
!(0, 255, 255);
1876 clr
= (fld.folderPath
== "accounts" ? gxRGB
!(220, 220, 0) : gxRGB
!(255, 127+60, 0));
1877 } else if (fld.folderPath
.startsWith("accounts/") && fld.folderPath
.endsWith("/inbox")) {
1878 clr
= gxRGB
!(255, 127+30, 0);
1881 foreach (immutable dd; 0..depth
) gxPutPixel(ofsx
+dd*6+2, ofsy
+gxTextHeightUtf
/2, gxRGB
!(80, 80, 80));
1882 gxDrawTextOutScaledUtf(1, ofsx
+depth
*6, ofsy
, folderVisName(idx
), clr
, gxRGB
!(0, 0, 0));
1883 ofsy
+= gxTextHeightUtf
+2;
1887 if (folderCur
< folders
.length
) drawThreadList(folders
[folderCur
]);
1890 override bool onKey (KeyEvent event
) {
1891 if (event
.pressed
) {
1892 if (dbg_dump_keynames
) conwriteln("key: ", event
.toStr
, ": ", event
.modifierState
&ModifierState
.windows
);
1893 //foreach (const ref kv; mainAppKeyBindings.byKeyValue) if (event == kv.key) concmd(kv.value);
1895 if (auto cmdp
= event
.toStrBuf(kname
[]) in mainAppKeyBindings
) {
1901 if (event == "S-Up") {
1902 if (folderTop > 0) --folderTop;
1903 postScreenRebuild();
1906 if (event == "S-Down") {
1907 if (folderTop+1 < folders.length) ++folderTop;
1908 postScreenRebuild();
1912 //if (event == "Tab") { new PostWindow(); return true; }
1913 //if (event == "Tab") { new SelectPopBoxWindow(null); return true; }
1918 // returning `false` to avoid screen rebuilding by dispatcher
1919 override bool onMouse (MouseEvent event
) {
1920 //FIXME: use window coordinates
1922 event
.mouse2xy(mx
, my
);
1924 if (event
.type
== MouseEventType
.buttonPressed
&& event
.button
== MouseButton
.left
) {
1926 if (mx
>= 0 && mx
< guiGroupListWidth
&& my
>= 0 && my
< VBufHeight
) {
1927 uint fnum
= my
/(gxTextHeightUtf
+2)+folderTop
;
1928 if (fnum
!= folderCur
) {
1930 postScreenRebuild();
1935 if (mx
> guiGroupListWidth
&& mx
< VBufWidth
&& my
>= 0 && my
< guiThreadListHeight
) {
1936 if (auto fld = getActiveFolder
) {
1937 my
/= gxTextHeightUtf
;
1938 if (fld.curidx
!= fld.msgtop
+my
) {
1939 fld.curidx
= fld.msgtop
+my
;
1940 if (fld.curidxValid
&& fld.curidxUnread
) fld.curidxUnread
= false;
1941 postScreenRebuild();
1948 if (event
.type
== MouseEventType
.buttonPressed
&& (event
.button
== MouseButton
.wheelUp || event
.button
== MouseButton
.wheelDown
)) {
1950 if (mx
>= 0 && mx
< guiGroupListWidth
&& my
>= 0 && my
< VBufHeight
) {
1951 if (event
.button
== MouseButton
.wheelUp
) {
1952 if (folderCur
> 0) folderCur
= folderCur
-1;
1954 if (folderCur
+1 < folders
.length
) folderCur
= folderCur
+1;
1956 postScreenRebuild();
1960 if (mx
> guiGroupListWidth
&& mx
< VBufWidth
&& my
>= 0 && my
< guiThreadListHeight
) {
1961 if (auto fld = getActiveFolder
) {
1962 if (event
.button
== MouseButton
.wheelUp
) fld.moveUp(); else fld.moveDown();
1963 postScreenRebuild();
1968 if (mx
> guiGroupListWidth
&& mx
< VBufWidth
&& my
> guiThreadListHeight
&& my
< VBufHeight
) {
1969 enum ScrollLines
= 2;
1970 if (event
.button
== MouseButton
.wheelUp
) scrollBy(-ScrollLines
); else scrollBy(ScrollLines
);
1971 postScreenRebuild();
1976 if (event
.type
== MouseEventType
.buttonReleased
&& event
.button
== MouseButton
.left
) {
1978 auto url
= findUrlAt(mx
, my
);
1981 concmdf
!"attach_save %s %s"(url
.attachnum
, ((event
.modifierState
&ModifierState
.alt
) != 0));
1983 if (event
.modifierState
&(ModifierState
.shift|ModifierState
.ctrl
)) {
1984 //conwriteln("link-to-clipboard: <", url.url, ">");
1985 setClipboardText(vbwin
, url
.url
); // it is safe to cast here
1986 setPrimarySelection(vbwin
, url
.url
); // it is safe to cast here
1987 conwriteln("url copied to the clipboard.");
1989 //conwriteln("link-open: <", url.url, ">");
1990 concmdf
!"open_url \"%s\" %s"(url
.url
, ((event
.modifierState
&ModifierState
.alt
) != 0));
1995 if (event
.type
== MouseEventType
.motion
) {
1996 auto uidx
= findUrlIndexAt(mx
, my
);
1997 if (uidx
!= lastUrlIndex
) { lastUrlIndex
= uidx
; postScreenRebuild(); return false; }
2000 if (event
.type
== MouseEventType
.buttonPressed || event
.type
== MouseEventType
.buttonReleased
) {
2001 postScreenRebuild();
2003 postScreenRepaint();
2009 override bool onChar (dchar ch
) {
2015 // ////////////////////////////////////////////////////////////////////////// //
2016 void rebuildScreen (SimpleWindow w
) {
2017 if (w
!is null && !w
.closed
&& !w
.hidden
) {
2018 static if (EGfxOpenGLBackend
) {
2019 w
.redrawOpenGlSceneNow();
2021 bool resizeWin
= false;
2024 scope(exit
) consoleUnlock();
2025 if (!conQueueEmpty()) postDoConCommands();
2026 if (vbufNewScale
!= vbufEffScale
) {
2027 // window scale changed
2030 if (vbufEffVSync
!= vbufVSync
) {
2031 vbufEffVSync
= vbufVSync
;
2035 w
.resize(winWidthScaled
, winHeightScaled
);
2036 glconResize(winWidthScaled
, winHeightScaled
);
2037 //vglResizeBuffer(VBufWidth, VBufHeight, vbufNewScale);
2039 /*if (egx11img !is null)*/ {
2040 //glconBackBuffer = egx11img;
2046 if (vArrowTextureId
) {
2047 if (isMouseVisible
) {
2048 int px
= lastMouseX
;
2049 int py
= lastMouseY
;
2050 //conwriteln("msalpha: ", mouseAlpha);
2051 //glColor4f(1, 1, 1, mouseAlpha);
2052 vglBlitArrow(px
, py
);
2057 auto painter
= w
.draw();
2059 glconDrawWindow
= w
;
2060 glconDrawDirect
= false;
2062 glconDrawWindow
= null;
2063 //painter.drawImage(Point(0, 0), egx11img);
2066 if (isQuitRequested()) w
.postEvent(new QuitEvent());
2073 void repaintScreen (SimpleWindow w
) {
2074 if (w
!is null && !w
.closed
&& !w
.hidden
) {
2075 static if (EGfxOpenGLBackend
) {
2076 w
.redrawOpenGlSceneNow();
2079 static bool lastvisible
= false;
2080 bool curvisible
= isConsoleVisible
;
2081 if (lastvisible
!= curvisible || curvisible
) {
2082 lastvisible
= curvisible
;
2091 // ////////////////////////////////////////////////////////////////////////// //
2092 __gshared LockFile mainLockFile
;
2095 void checkMainLockFile () {
2096 import std
.path
: buildPath
;
2097 mainLockFile
= LockFile(buildPath(mailRootDir
, ".chiroptera.lock"));
2098 if (!mainLockFile
.tryLock
) { mainLockFile
.close(); assert(0, "already running"); }
2102 void main (string
[] args
) {
2104 // it is completely fucked
2105 pragma(msg
, "MSG: fucked compiler");
2107 import etc
.linux
.memoryerror
;
2110 while (idx
< args
.length
) {
2111 string a
= args
[idx
++];
2112 if (a
== "--") break;
2116 foreach (immutable c
; idx
+1..args
.length
) args
[c
-1] = args
[c
];
2119 if (setMH
) registerMemoryErrorHandler();
2122 glconAllowOpenGLRender
= false;
2124 checkMainLockFile();
2125 scope(exit
) mainLockFile
.close();
2127 sdpyWindowClass
= "Chiroptera";
2128 //glconShowKey = "M-Grave";
2131 hitwitInitConsole();
2134 setupDefaultBindings();
2136 concmd("exec chiroptera.rc tan");
2140 concmdf
!"exec %s/accounts.rc tan"(mailRootDir
);
2141 concmdf
!"exec %s/addressbook.rc tan"(mailRootDir
);
2142 concmdf
!"exec %s/filters.rc tan"(mailRootDir
);
2143 concmdf
!"exec %s/highlights.rc tan"(mailRootDir
);
2144 concmdf
!"exec %s/twits.rc tan"(mailRootDir
);
2145 concmdf
!"exec %s/twit_threads.rc tan"(mailRootDir
);
2146 concmdf
!"exec %s/auto_twits.rc tan"(mailRootDir
);
2147 concmdf
!"exec %s/auto_twit_threads.rc tan"(mailRootDir
);
2148 concmdf
!"exec %s/repreps.rc tan"(mailRootDir
);
2149 conProcessQueue(); // load config
2150 conProcessArgs
!true(args
);
2152 vbufEffScale
= vbufNewScale
;
2153 vbufEffVSync
= vbufVSync
;
2155 lastWinWidth
= winWidthScaled
;
2156 lastWinHeight
= winHeightScaled
;
2158 restoreCurrentFolderAndPosition();
2160 static if (EGfxOpenGLBackend
) {
2161 vbwin
= new SimpleWindow(lastWinWidth
, lastWinHeight
, "Chiroptera", OpenGlOptions
.yes
, Resizability
.allowResizing
);
2164 vbwin
= new SimpleWindow(lastWinWidth
, lastWinHeight
, "Chiroptera", OpenGlOptions
.no
, Resizability
.allowResizing
);
2167 vbwin
.onFocusChange
= delegate (bool focused
) {
2168 vbfocused
= focused
;
2170 lastMouseButton
= 0;
2171 eguiLostGlobalFocus();
2175 vbwin
.windowResized
= delegate (int wdt
, int hgt
) {
2176 // TODO: fix gui sizes
2177 if (vbwin
.closed
) return;
2179 if (lastWinWidth
== wdt
&& lastWinHeight
== hgt
) return;
2180 glconResize(wdt
, hgt
);
2182 double glwFrac
= cast(double)guiGroupListWidth
/VBufWidth
;
2183 double tlhFrac
= cast(double)guiThreadListHeight
/VBufHeight
;
2185 if (wdt
< vbufNewScale
*32) wdt
= vbufNewScale
;
2186 if (hgt
< vbufNewScale
*32) hgt
= vbufNewScale
;
2187 int newwdt
= (wdt
+vbufNewScale
-1)/vbufNewScale
;
2188 int newhgt
= (hgt
+vbufNewScale
-1)/vbufNewScale
;
2190 guiGroupListWidth
= cast(int)(glwFrac
*newwdt
+0.5);
2191 guiThreadListHeight
= cast(int)(tlhFrac
*newhgt
+0.5);
2193 if (guiGroupListWidth
< 12) guiGroupListWidth
= 12;
2194 if (guiThreadListHeight
< 16) guiThreadListHeight
= 16;
2197 lastWinHeight
= hgt
;
2199 vglResizeBuffer(newwdt
, newhgt
, vbufNewScale
);
2203 rebuildScreen(vbwin
);
2206 vbwin
.addEventListener((DoConsoleCommandsEvent evt
) {
2207 bool sendAnother
= false;
2208 bool prevVisible
= isConsoleVisible
;
2211 scope(exit
) consoleUnlock();
2213 sendAnother
= !conQueueEmpty();
2215 if (sendAnother
) postDoConCommands();
2216 if (vbwin
.closed
) return;
2217 if (isQuitRequested
) { vbwin
.close(); return; }
2218 if (prevVisible || isConsoleVisible
) postScreenRepaintDelayed();
2221 vbwin
.addEventListener((HideMouseEvent evt
) {
2222 if (vbwin
.closed
) return;
2223 if (isQuitRequested
) { vbwin
.close(); return; }
2224 if (repostHideMouse
) {
2225 //conwriteln("HME: visible=", isMouseVisible, "; alpha=", mouseAlpha, "; tm=", mshtime_dbg);
2226 if (mainPane
!is null && !isMouseVisible
) mainPane
.lastUrlIndex
= -1;
2227 repaintScreen(vbwin
);
2231 vbwin
.addEventListener((ScreenRebuildEvent evt
) {
2232 if (vbwin
.closed
) return;
2233 if (isQuitRequested
) { vbwin
.close(); return; }
2234 rebuildScreen(vbwin
);
2235 if (isConsoleVisible
) postScreenRepaintDelayed();
2238 vbwin
.addEventListener((ScreenRepaintEvent evt
) {
2239 if (vbwin
.closed
) return;
2240 if (isQuitRequested
) { vbwin
.close(); return; }
2241 repaintScreen(vbwin
);
2242 if (isConsoleVisible
) postScreenRepaintDelayed();
2245 vbwin
.addEventListener((CursorBlinkEvent evt
) {
2246 if (vbwin
.closed
) return;
2247 rebuildScreen(vbwin
);
2250 vbwin
.addEventListener((QuitEvent evt
) {
2251 if (vbwin
.closed
) return;
2252 if (isQuitRequested
) { vbwin
.close(); return; }
2257 vbwin
.addEventListener((UnreadChangedEvent evt
) {
2258 if (vbwin
.closed
) return;
2259 if (isQuitRequested
) { vbwin
.close(); return; }
2260 setupTrayAnimation();
2263 vbwin
.addEventListener((TrayStartAnimationEvent evt
) {
2264 if (vbwin
.closed
) return;
2265 if (isQuitRequested
) { vbwin
.close(); return; }
2266 trayStartAnimation();
2269 vbwin
.addEventListener((TrayStopAnimationEvent evt
) {
2270 if (vbwin
.closed
) return;
2271 if (isQuitRequested
) { vbwin
.close(); return; }
2272 trayStopAnimation();
2275 vbwin
.addEventListener((TraySetupAnimationEvent evt
) {
2276 if (vbwin
.closed
) return;
2277 if (isQuitRequested
) { vbwin
.close(); return; }
2278 setupTrayAnimation();
2281 vbwin
.addEventListener((TrayAnimationStepEvent evt
) {
2282 if (vbwin
.closed
) return;
2283 if (isQuitRequested
) { vbwin
.close(); return; }
2284 trayDoAnimationStep();
2287 HintWindow uphintWindow
;
2289 vbwin
.addEventListener((UpdatingAccountEvent evt
) {
2290 if (evt
.accName
.length
) {
2291 if (uphintWindow
!is null) {
2292 uphintWindow
.message
= "updating: "~evt
.accName
;
2294 uphintWindow
= new HintWindow("updating: "~evt
.accName
);
2295 uphintWindow
.winy
= guiThreadListHeight
+1+(3*gxTextHeightUtf
+2-uphintWindow
.winh
)/2;
2297 postScreenRebuild();
2301 vbwin
.addEventListener((UpdatingAccountCompleteEvent evt
) {
2302 if (evt
.accName
.length
&& uphintWindow
!is null) {
2303 uphintWindow
.message
= "done: "~evt
.accName
;
2304 postScreenRebuild();
2308 vbwin
.addEventListener((UpdatingCompleteEvent evt
) {
2310 uphintWindow
.close();
2311 uphintWindow
= null;
2313 foreach (Folder
fld; folders
) if (fld.needRebuild
) fld.buildVisibleList();
2314 setupTrayAnimation(); // check if we have to start/stop animation, and do it
2315 rebuildScreen(vbwin
);
2318 vbwin
.addEventListener((DoCheckBoxesCycleEvent evt
) {
2319 //conwriteln("updating group '", groups[evt.gidx].mbase.groupname);
2320 if (!atomicLoad(updateInProgress
)) {
2321 updateThreadId
.send(UpThreadCommand
.StartUpdate
);
2325 vbwin
.addEventListener((ArticleTextScrollEvent evt
) {
2326 if (vbwin
is null || vbwin
.closed
) return;
2327 if (mainPane
is null) return;
2328 mainPane
.doScrollStep();
2331 vbwin
.addEventListener((MarkAsUnreadEvent evt
) {
2332 if (vbwin
is null || vbwin
.closed
) return;
2333 if (mainPane
is null) return;
2334 //conwriteln("unread timer fired");
2335 if (evt
.folder
is getActiveFolder
&& evt
.idx
== evt
.folder
.curidx
) {
2336 //conwriteln("*** unread timer hit!");
2337 evt
.folder
.curidxUnread
= false;
2338 postScreenRebuild();
2342 vbwin
.redrawOpenGlScene
= delegate () {
2343 if (vbwin
.closed
) return;
2345 bool resizeWin
= false;
2349 scope(exit
) consoleUnlock();
2351 if (!conQueueEmpty()) postDoConCommands();
2353 if (vbufNewScale
!= vbufEffScale
) {
2354 // window scale changed
2357 if (vbufEffVSync
!= vbufVSync
) {
2358 vbufEffVSync
= vbufVSync
;
2359 vbwin
.vsync
= vbufEffVSync
;
2364 vbwin
.resize(winWidthScaled
, winHeightScaled
);
2365 glconResize(winWidthScaled
, winHeightScaled
);
2366 //vglResizeBuffer(VBufWidth, VBufHeight, vbufNewScale);
2369 //if (rebuildTexture) repaintScreen();
2371 vglBlitTexture(vbwin
);
2373 if (vArrowTextureId
) {
2374 if (isMouseVisible
) {
2375 int px
= lastMouseX
;
2376 int py
= lastMouseY
;
2377 //conwriteln("msalpha: ", mouseAlpha);
2378 glColor4f(1, 1, 1, mouseAlpha
);
2379 vglBlitArrow(px
, py
);
2385 if (isQuitRequested()) vbwin
.postEvent(new QuitEvent());
2388 static if (is(typeof(&vbwin
.closeQuery
))) {
2389 vbwin
.closeQuery
= delegate () { concmd("quit"); postDoConCommands(); };
2392 void firstTimeInit () {
2393 static bool firstTimeInited
= false;
2394 if (firstTimeInited
) return;
2395 firstTimeInited
= true;
2397 static if (EGfxOpenGLBackend
) {
2399 vbwin
.setAsCurrentOpenGlContext();
2400 vbwin
.vsync
= vbufEffVSync
;
2402 vbufEffVSync
= vbufVSync
;
2404 vglResizeBuffer(VBufWidth
, VBufHeight
);
2405 vglCreateArrowTexture();
2407 glconInit(winWidthScaled
, winHeightScaled
);
2409 rebuildScreen(vbwin
);
2411 updateThreadId
= spawn(&updateThread
, thisTid
);
2413 // create notification icon
2414 if (trayicon
is null) {
2415 auto drv
= vfsAddPak(wrapMemoryRO(iconsZipData
[]), "", "databinz/icons.zip:");
2416 scope(exit
) vfsRemovePak(drv
);
2418 foreach (immutable idx
; 0..6) {
2419 string fname
= "databinz/icons.zip:icons";
2421 fname
~= "/main.png";
2423 import std
.format
: format
;
2424 fname
= "%s/bat%s.png".format(fname
, idx
-1);
2426 auto fl
= VFile(fname
);
2427 if (fl
.size
== 0 || fl
.size
> 1024*1024) throw new Exception("fucked icon");
2428 auto pngraw
= new ubyte[](cast(uint)fl
.size
);
2429 fl
.rawReadExact(pngraw
);
2430 auto img
= readPng(pngraw
);
2431 if (img
is null) throw new Exception("fucked icon");
2432 icons
[idx
] = imageFromPng(img
);
2434 foreach (immutable idx
, MemoryImage img
; icons
[]) {
2435 trayimages
[idx
] = Image
.fromMemoryImage(img
);
2437 vbwin
.icon
= icons
[0];
2438 trayicon
= new NotificationAreaIcon("Chiroptera", trayimages
[0], (MouseButton button
) {
2439 scope(exit
) if (!conQueueEmpty()) postDoConCommands();
2440 if (button
== MouseButton
.left
) vbwin
.switchToWindow();
2441 if (button
== MouseButton
.middle
) concmd("quit");
2443 setupTrayAnimation();
2444 flushGui(); // or it may not redraw itself
2445 } catch (Exception e
) {
2446 conwriteln("ERROR loading icons: ", e
.msg
);
2451 vbwin
.visibleForTheFirstTime
= delegate () {
2455 mainPane
= new MainPaneWindow();
2457 postScreenRebuild();
2460 vbwin
.eventLoop(1000*10,
2462 scope(exit
) if (!conQueueEmpty()) postDoConCommands();
2463 if (vbwin
.closed
) return;
2466 scope(exit
) consoleUnlock();
2469 if (isQuitRequested
) { vbwin
.close(); return; }
2471 delegate (KeyEvent event
) {
2472 scope(exit
) if (!conQueueEmpty()) postDoConCommands();
2473 if (vbwin
.closed
) return;
2474 if (isQuitRequested
) { vbwin
.close(); return; }
2475 if (glconKeyEvent(event
)) {
2476 postScreenRepaint();
2479 if ((event
.modifierState
&ModifierState
.numLock
) == 0) {
2480 switch (event
.key
) {
2481 case Key
.Pad0
: event
.key
= Key
.Insert
; break;
2482 case Key
.Pad1
: event
.key
= Key
.End
; break;
2483 case Key
.Pad2
: event
.key
= Key
.Down
; break;
2484 case Key
.Pad3
: event
.key
= Key
.PageDown
; break;
2485 case Key
.Pad4
: event
.key
= Key
.Left
; break;
2486 //case Key.Pad5: event.key = Key.Insert; break;
2487 case Key
.Pad6
: event
.key
= Key
.Right
; break;
2488 case Key
.Pad7
: event
.key
= Key
.Home
; break;
2489 case Key
.Pad8
: event
.key
= Key
.Up
; break;
2490 case Key
.Pad9
: event
.key
= Key
.PageUp
; break;
2491 case Key
.PadEnter
: event
.key
= Key
.Enter
; break;
2492 case Key
.PadDot
: event
.key
= Key
.Delete
; break;
2496 if (event
.key
== Key
.PadEnter
) event
.key
= Key
.Enter
;
2498 if (dispatchEvent(event
)) return;
2499 //postScreenRepaint(); // just in case
2501 delegate (MouseEvent event
) {
2502 scope(exit
) if (!conQueueEmpty()) postDoConCommands();
2503 if (vbwin
.closed
) return;
2504 lastMouseXUnscaled
= event
.x
;
2505 lastMouseYUnscaled
= event
.y
;
2506 if (event
.type
== MouseEventType
.buttonPressed
) lastMouseButton |
= event
.button
;
2507 else if (event
.type
== MouseEventType
.buttonReleased
) lastMouseButton
&= ~event
.button
;
2509 if (dispatchEvent(event
)) return;
2511 delegate (dchar ch
) {
2512 if (vbwin
.closed
) return;
2513 scope(exit
) if (!conQueueEmpty()) postDoConCommands();
2514 if (glconCharEvent(ch
)) {
2515 postScreenRepaint();
2518 if (dispatchEvent(ch
)) return;
2521 saveCurrentFolderAndPosition();
2522 trayimages
[] = null;
2523 if (trayicon
!is null && !trayicon
.closed
) { trayicon
.close(); trayicon
= null; }
2525 updateThreadId
.send(UpThreadCommand
.Quit
);
2526 conProcessQueue(int.max
/4);