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, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 module chiroptera
is aliced
;
22 import std
.concurrency
;
25 //import arsd.htmltotext;
26 import arsd
.simpledisplay
;
52 // ////////////////////////////////////////////////////////////////////////// //
53 static immutable ubyte[] iconsZipData
= cast(immutable(ubyte)[])import("databin/icons.zip");
56 // ////////////////////////////////////////////////////////////////////////// //
57 string
getBrowserCommand () {
58 __gshared string browser
;
59 if (browser
.length
== 0) {
60 import core
.stdc
.stdlib
: getenv
;
61 const(char)* evar
= getenv("BROWSER");
62 if (evar
!is null && evar
[0]) {
63 import std
.string
: fromStringz
;
64 browser
= evar
.fromStringz
.idup
;
73 // ////////////////////////////////////////////////////////////////////////// //
74 __gshared
bool dbg_dump_keynames
;
77 // ////////////////////////////////////////////////////////////////////////// //
78 class TrayAnimationStepEvent
{}
79 __gshared TrayAnimationStepEvent evTrayAnimationStep
;
80 shared static this () { evTrayAnimationStep
= new TrayAnimationStepEvent(); }
82 __gshared Tid updateThreadId
;
83 shared bool updateInProgress
= false;
84 __gshared
int trayAnimationIndex
= 0; // 0: no animation
85 __gshared
int trayAnimationDir
= 1; // direction
88 // ////////////////////////////////////////////////////////////////////////// //
89 void trayPostAnimationEvent () {
90 if (vbwin
!is null && !vbwin
.eventQueued
!TrayAnimationStepEvent
) vbwin
.postTimeout(evTrayAnimationStep
, 100);
94 void trayDoAnimationStep () {
95 if (trayicon
is null || trayicon
.closed
) return; // no tray icon
96 if (vbwin
is null || vbwin
.closed
) return;
97 if (trayAnimationIndex
== 0) return; // no animation
98 trayPostAnimationEvent();
99 if (trayAnimationDir
< 0) {
100 if (--trayAnimationIndex
== 1) trayAnimationDir
= 1;
102 if (++trayAnimationIndex
== trayimages
.length
-1) trayAnimationDir
= -1;
104 trayicon
.icon
= trayimages
[trayAnimationIndex
];
105 //vbwin.icon = icons[trayAnimationIndex];
106 //vbwin.sendDummyEvent(); // or it won't redraw itself
107 flushGui(); // or it may not redraw itself
111 // ////////////////////////////////////////////////////////////////////////// //
112 void trayStartAnimation () {
113 if (trayicon
is null) return; // no tray icon
114 if (trayAnimationIndex
== 0) {
115 trayAnimationIndex
= 1;
116 trayAnimationDir
= 1;
117 trayicon
.icon
= trayimages
[1];
118 vbwin
.icon
= icons
[1];
119 //vbwin.sendDummyEvent(); // or it may not redraw itself
120 flushGui(); // or it may not redraw itself
121 trayPostAnimationEvent();
126 void trayStopAnimation () {
127 if (trayicon
is null) return; // no tray icon
128 if (trayAnimationIndex
!= 0) {
129 trayAnimationIndex
= 0;
130 trayAnimationDir
= 1;
131 trayicon
.icon
= trayimages
[0];
132 vbwin
.icon
= icons
[0];
133 //vbwin.sendDummyEvent(); // or it may not redraw itself
134 flushGui(); // or it may not redraw itself
139 // check if we have to start/stop animation, and do it
140 void setupTrayAnimation () {
141 foreach (Folder
fld; folders
) {
142 if (fld.unreadCount
> 0) { trayStartAnimation(); return; }
148 // ////////////////////////////////////////////////////////////////////////// //
149 __gshared string
[string
] mainAppKeyBindings
;
151 void clearBindings () {
152 mainAppKeyBindings
.clear();
156 void mainAppBind (ConString kname
, ConString concmd
) {
157 KeyEvent evt
= KeyEvent
.parse(kname
);
159 mainAppKeyBindings
[evt
.toStr
] = concmd
.idup
;
161 mainAppKeyBindings
.remove(evt
.toStr
);
166 void mainAppUnbind (ConString kname
) {
167 KeyEvent evt
= KeyEvent
.parse(kname
);
168 mainAppKeyBindings
.remove(evt
.toStr
);
172 void setupDefaultBindings () {
173 mainAppBind("C-L", "dbg_font_window");
174 mainAppBind("C-Q", "quit_prompt");
175 mainAppBind("N", "next_unread ona");
176 mainAppBind("S-N", "next_unread tan");
177 mainAppBind("U", "mark_unread");
178 mainAppBind("C-R", "mark_read");
179 mainAppBind("Space", "artext_page_down");
180 mainAppBind("S-Space", "artext_page_up");
181 mainAppBind("M-Up", "artext_line_up");
182 mainAppBind("M-Down", "artext_line_down");
183 mainAppBind("Up", "article_prev");
184 mainAppBind("Down", "article_next");
185 mainAppBind("PageUp", "article_pgup");
186 mainAppBind("PageDown", "article_pgdown");
187 mainAppBind("Home", "article_to_first");
188 mainAppBind("End", "article_to_last");
189 mainAppBind("C-Up", "article_scroll_up");
190 mainAppBind("C-Down", "article_scroll_down");
191 mainAppBind("C-PageUp", "folder_prev");
192 mainAppBind("C-PageDown", "folder_next");
193 mainAppBind("C-M-U", "folder_update");
194 mainAppBind("C-H", "article_dump_headers");
195 mainAppBind("C-S-I", "update_all");
196 mainAppBind("C-Backslash", "find_mine");
197 mainAppBind("C-Slash", "article_to_parent");
198 mainAppBind("C-Comma", "article_to_prev_sib");
199 mainAppBind("C-Period", "article_to_next_sib");
200 mainAppBind("C-Insert", "article_copy_url_to_clipboard");
201 mainAppBind("C-M-K", "article_twit_thread");
202 mainAppBind("T", "article_edit_poster_title");
203 mainAppBind("R", "article_reply");
204 mainAppBind("S-P", "new_post");
205 mainAppBind("S-Enter", "article_open_in_browser");
206 mainAppBind("Delete", "article_softdelete_toggle");
210 // ////////////////////////////////////////////////////////////////////////// //
211 enum UpThreadCommand
{
213 StartUpdate
, // start updating now
217 void updateThread (Tid ownerTid
) {
221 MonoTime lastCollect
= MonoTime
.currTime
;
223 receiveTimeout(30.seconds
,
224 (UpThreadCommand cmd
) {
226 case UpThreadCommand
.Ping
: break;
227 case UpThreadCommand
.StartUpdate
: break;
228 case UpThreadCommand
.Quit
: doQuit
= true; break;
233 bool updateProgressSet
= false;
234 foreach (Account acc
; accounts
) {
235 if (acc
.needUpdate
) {
236 updateProgressSet
= true;
237 atomicStore(updateInProgress
, true);
239 if (vbwin
!is null) vbwin
.postEvent(new UpdatingAccountEvent(acc
.name
));
241 } catch (Exception e
) {
242 conwriteln("ERROR UPDATING ACCOUNT '", acc
.name
, "': ", e
.msg
);
244 if (vbwin
!is null) vbwin
.postEvent(new UpdatingAccountCompleteEvent(acc
.name
));
247 if (updateProgressSet
) {
248 if (vbwin
!is null) vbwin
.postEvent(new UpdatingCompleteEvent());
249 atomicStore(updateInProgress
, false);
252 auto ctt
= MonoTime
.currTime
;
253 if ((ctt
-lastCollect
).total
!"minutes" >= 5) {
254 import core
.memory
: GC
;
261 } catch (Throwable e
) {
262 // here, we are dead and fucked (the exact order doesn't matter)
263 import core
.stdc
.stdlib
: abort
;
264 import core
.stdc
.stdio
: fprintf
, stderr
;
265 import core
.memory
: GC
;
266 import core
.thread
: thread_suspendAll
;
267 GC
.disable(); // yeah
268 thread_suspendAll(); // stop right here, you criminal scum!
269 auto s
= e
.toString();
270 fprintf(stderr
, "\n=== FATAL ===\n%.*s\n", cast(uint)s
.length
, s
.ptr
);
271 abort(); // die, you bitch!
276 // ////////////////////////////////////////////////////////////////////////// //
277 void initConsole () {
278 import std
.functional
: toDelegate
;
280 conRegVar
!VBufScale(1, 4, "v_scale", "window scale: [1..3]");
282 conRegVar
!bool("v_vsync", "sync to video refresh rate?",
283 (ConVarBase self
) => vbufVSync
,
284 (ConVarBase self
, bool nv
) {
285 if (vbufVSync
!= nv
) {
292 conRegFunc
!clearBindings("binds_app_clear", "clear main application keybindings");
293 conRegFunc
!setupDefaultBindings("binds_app_default", "*append* default application bindings");
294 conRegFunc
!mainAppBind("bind_app", "add main application binding");
295 conRegFunc
!mainAppUnbind("unbind_app", "remove main application binding");
298 // //////////////////////////////////////////////////////////////////// //
300 import core
.memory
: GC
;
301 conwriteln("starting GC collection...");
304 conwriteln("GC collection complete.");
305 })("gc_collect", "force GC collection cycle");
308 // //////////////////////////////////////////////////////////////////// //
310 auto qww
= new YesNoWindow("Quit?", "Do you really want to quit?", true);
311 qww
.onYes
= () { concmd("quit"); };
313 })("quit_prompt", "quit with prompt");
316 // //////////////////////////////////////////////////////////////////// //
317 conRegFunc
!((ConString url
) {
319 import std
.stdio
: File
;
321 auto pid
= spawnProcess(
322 [getBrowserCommand
, url
.idup
],
323 File("/dev/zero"), File("/dev/null"), File("/dev/null"),
327 })("open_url", "open given url in a browser");
330 // //////////////////////////////////////////////////////////////////// //
332 foreach (Account acc
; accounts
) acc
.forceUpdating();
333 if (!atomicLoad(updateInProgress
)) {
334 updateThreadId
.send(UpThreadCommand
.StartUpdate
);
336 })("update_all", "mark all groups for updating");
339 // //////////////////////////////////////////////////////////////////// //
341 if (folderCur
> 0) folderCur
= folderCur
-1;
343 })("folder_prev", "go to previous group");
346 if (folders
.length
-folderCur
> 1) folderCur
= folderCur
+1;
348 })("folder_next", "go to next group");
351 // //////////////////////////////////////////////////////////////////// //
353 if (auto fld = getActiveFolder
) {
354 if (fld.markAsUnread()) postScreenRebuild();
356 })("mark_unread", "mark current message as unread");
359 if (auto fld = getActiveFolder
) {
360 if (fld.markAsRead()) postScreenRebuild();
362 })("mark_read", "mark current message as read");
364 conRegFunc
!((bool allowNextGroup
=false) {
365 if (auto fld = getActiveFolder
) {
366 if (!fld.moveToNextUnread(true)) {
367 if (!allowNextGroup
) return;
369 uint fidx
= cast(uint)((folderCur
+1)%folders
.length
);
370 foreach (immutable _
; 0..folders
.length
) {
371 if (folders
[fidx
].moveToNextUnread(true)) {
376 fidx
= cast(uint)((fidx
+1)%folders
.length
);
382 })("next_unread", "move to next unread message; bool arg: can skip to next group?");
385 // //////////////////////////////////////////////////////////////////// //
387 if (mainPane
!is null) mainPane
.scrollByPageUp();
388 })("artext_page_up", "do pageup on article text");
391 if (mainPane
!is null) mainPane
.scrollByPageDown();
392 })("artext_page_down", "do pagedown on article text");
395 if (mainPane
!is null) mainPane
.scrollBy(-1);
396 })("artext_line_up", "do lineup on article text");
399 if (mainPane
!is null) mainPane
.scrollBy(1);
400 })("artext_line_down", "do linedown on article text");
402 // //////////////////////////////////////////////////////////////////// //
404 if (auto fldx
= getActiveFolder
) {
405 fldx
.withBaseReader((abase
, cur
, top
, alist
) {
406 if (cur
< alist
.length
) {
407 abase
.loadContent(alist
[cur
]);
408 if (auto art
= abase
[alist
[cur
]]) {
409 scope(exit
) art
.releaseContent();
410 auto path
= art
.getHeaderValue("path:");
411 //conwriteln("path: [", path, "]");
412 if (path
.startsWithCI("digitalmars.com!.POSTED.")) {
413 import std
.stdio
: File
;
415 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
416 string id
= art
.msgid
;
417 if (id
.length
> 2 && id
[0] == '<' && id
[$-1] == '>') id
= id
[1..$-1];
418 auto pid
= spawnProcess(
419 [getBrowserCommand
, "http://forum.dlang.org/post/"~id
],
420 File("/dev/zero"), File("/dev/null"), File("/dev/null"),
428 })("article_open_in_browser", "open the current article in browser");
431 if (auto fldx
= getActiveFolder
) {
432 fldx
.withBaseReader((abase
, cur
, top
, alist
) {
433 if (cur
< alist
.length
) {
434 if (auto art
= abase
[alist
[cur
]]) {
435 auto path
= art
.getHeaderValue("path:");
436 if (path
.startsWithCI("digitalmars.com!.POSTED.")) {
437 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
438 string id
= art
.msgid
;
439 if (id
.length
> 2 && id
[0] == '<' && id
[$-1] == '>') id
= id
[1..$-1];
440 id
= "http://forum.dlang.org/post/"~id
;
441 setClipboardText(vbwin
, id
);
442 setPrimarySelection(vbwin
, id
);
448 })("article_copy_url_to_clipboard", "copy the url of the current article to clipboard");
451 // //////////////////////////////////////////////////////////////////// //
453 if (auto fld = getActiveFolder
) {
457 })("article_prev", "go to previous article");
460 if (auto fld = getActiveFolder
) {
464 })("article_next", "go to next article");
467 if (auto fld = getActiveFolder
) {
471 })("article_pgup", "artiles list: page up");
474 if (auto fld = getActiveFolder
) {
478 })("article_pgdown", "artiles list: page down");
481 if (auto fld = getActiveFolder
) {
485 })("article_scroll_up", "scroll article list up");
488 if (auto fld = getActiveFolder
) {
492 })("article_scroll_down", "scroll article list up");
495 if (auto fld = getActiveFolder
) {
499 })("article_to_first", "go to first article");
502 if (auto fld = getActiveFolder
) {
506 })("article_to_last", "go to last article");
509 // //////////////////////////////////////////////////////////////////// //
511 if (auto fld = getActiveFolder
) {
512 auto postDg
= delegate (Account acc
) {
513 conwriteln("post with account '", acc
.name
, "' (", acc
.mail
, ")");
514 auto pw
= new PostWindow();
515 pw
.from
.str = acc
.realname
~" <"~acc
.mail
~">";
516 pw
.from
.readonly
= true;
517 if (auto nna
= cast(NntpAccount
)acc
) {
518 pw
.title
= "Reply to NNTP "~nna
.server
~":"~nna
.group
;
519 pw
.to
.str = nna
.group
;
520 pw
.to
.readonly
= true;
521 pw
.activeWidget
= pw
.subj
;
523 pw
.title
= "Reply from '"~acc
.name
~"' ("~acc
.mail
~")";
525 pw
.activeWidget
= pw
.to
;
532 auto acc
= fld.findAccountToPost();
534 auto wacc
= new SelectPopBoxWindow(defaultAcc
);
535 wacc
.onSelected
= postDg
;
541 conwriteln("post: no active folder");
543 })("new_post", "post a new article or message");
546 // //////////////////////////////////////////////////////////////////// //
548 if (auto fld = getActiveFolder
) {
549 auto postDg
= delegate (Account acc
) {
550 conwriteln("reply with account '", acc
.name
, "' (", acc
.mail
, ")");
551 fld.withBaseReader((abase
, cur
, top
, alist
) {
552 if (cur
< alist
.length
) {
553 auto aidx
= alist
.ptr
[cur
];
554 abase
.loadContent(aidx
);
555 if (auto art
= abase
[aidx
]) {
556 assert(art
.contentLoaded
);
557 auto atext
= art
.getTextContent
;
558 auto pw
= new PostWindow();
559 pw
.from
.str = acc
.realname
~" <"~acc
.mail
~">";
560 pw
.from
.readonly
= true;
561 if (auto nna
= cast(NntpAccount
)acc
) {
562 pw
.title
= "Reply to NNTP "~nna
.server
~":"~nna
.group
;
563 pw
.to
.str = nna
.group
;
564 pw
.to
.readonly
= true;
566 pw
.title
= "Reply from '"~acc
.name
~"' ("~acc
.mail
~")";
567 pw
.to
.str = art
.fromname
~" <"~art
.frommail
~">";
569 pw
.subj
.str = "Re: "~art
.subj
;
571 string from
= art
.fromname
;
573 auto vp
= from
.indexOf(" via Digitalmars-");
575 from
= from
[0..vp
].xstrip
;
576 if (from
.length
== 0) from
= "anonymous";
581 pw
.ed
.addText(" wrote:\n");
583 foreach (ConString s
; LineIterator
!false(atext
)) {
584 if (s
.length
== 0 || s
[0] == '>') pw
.ed
.addText(">"); else pw
.ed
.addText("> ");
590 pw
.replyto
= art
.msgid
;
591 pw
.references
= art
.getHeaderValue("References").xstrip
.idup
;
594 pw
.activeWidget
= pw
.ed
;
600 auto acc
= fld.findAccountToPost(fld.curidx
);
602 auto wacc
= new SelectPopBoxWindow(defaultAcc
);
603 wacc
.onSelected
= postDg
;
609 })("article_reply", "reply to the current article");
612 // //////////////////////////////////////////////////////////////////// //
614 if (auto fld = getActiveFolder
) {
615 fld.withBaseReader((abase
, cur
, top
, alist
) {
616 if (cur
< alist
.length
) {
617 auto aidx
= alist
.ptr
[cur
];
618 abase
.loadContent(aidx
);
619 if (auto art
= abase
[aidx
]) {
620 assert(art
.contentLoaded
);
621 articleBogoMarkHam(art
);
622 //TODO: move out of spam
627 })("article_mark_ham", "mark current article as ham");
631 if (auto fld = getActiveFolder
) {
633 uint didx
= uint.max
;
634 Folder fldspam
= getSpamFolder
;
635 fld.withBaseReader((abase
, cur
, top
, alist
) {
636 if (cur
< alist
.length
) {
637 auto aidx
= alist
.ptr
[cur
];
638 abase
.loadContent(aidx
);
639 if (auto art
= abase
[aidx
]) {
640 assert(art
.contentLoaded
);
641 conwriteln("marking ", art
.msgid
, " as spam");
642 articleBogoMarkSpam(art
);
643 if (fld !is fldspam
) {
644 conwriteln(" should be moved to spam folder");
645 newart
= art
.clone();
652 if (didx
!= uint.max
) {
653 assert(newart
!is null);
654 conwriteln("removing ", newart
.msgid
, " from ", fld.folderPath
);
655 fld.withBaseWriter((abase
) {
656 abase
.softDeleted(didx
, true);
657 abase
.writeUpdates();
659 fld.markForRebuild();
660 fld.buildVisibleList();
663 if (newart
!is null && fldspam
!is null) {
664 newart
.unread
= false;
665 conwriteln("adding ", newart
.msgid
, " to ", fldspam
.folderPath
);
666 fldspam
.withBaseWriter((abase
) {
667 abase
.insert(newart
);
668 abase
.writeUpdates();
670 newart
.releaseContent();
671 fldspam
.markForRebuild();
672 fldspam
.buildVisibleList();
676 })("article_mark_spam", "mark current article as spam");
679 // //////////////////////////////////////////////////////////////////// //
680 conRegFunc
!((ConString fldname
) {
681 if (auto fld = getActiveFolder
) {
682 Folder destfld
= findFolderByPath(fldname
);
683 if (destfld
is null) { conwriteln("cannot find folder '", fldname
, "'"); }
684 if (destfld
is fld) { conwriteln("cannot move to the same folder"); return; }
686 uint didx
= uint.max
;
687 fld.withBaseReader((abase
, cur
, top
, alist
) {
688 if (cur
< alist
.length
) {
689 auto aidx
= alist
.ptr
[cur
];
690 abase
.loadContent(aidx
);
691 if (auto art
= abase
[aidx
]) {
692 assert(art
.contentLoaded
);
693 newart
= art
.clone();
698 if (didx
== uint.max || newart
is null) { conwriteln("article not found!"); return; }
700 assert(didx
!= uint.max
);
701 assert(newart
!is null);
702 conwriteln("removing ", newart
.msgid
, " from ", fld.folderPath
);
703 fld.withBaseWriter((abase
) {
704 abase
.softDeleted(didx
, true);
705 abase
.writeUpdates();
707 fld.markForRebuild();
708 fld.buildVisibleList();
710 conwriteln("adding ", newart
.msgid
, " to ", destfld
.folderPath
);
711 destfld
.withBaseWriter((abase
) {
712 abase
.insert(newart
);
713 abase
.writeUpdates();
715 newart
.releaseContent();
716 destfld
.markForRebuild();
717 destfld
.buildVisibleList();
720 })("article_move_to_folder", "move article to existing folder");
723 // //////////////////////////////////////////////////////////////////// //
725 if (auto fld = getActiveFolder
) {
729 })("article_to_parent", "jump to parent article, if any");
732 if (auto fld = getActiveFolder
) {
736 })("article_to_prev_sib", "jump to previous sibling");
739 if (auto fld = getActiveFolder
) {
743 })("article_to_next_sib", "jump to next sibling");
746 // //////////////////////////////////////////////////////////////////// //
748 if (auto fld = getActiveFolder
) {
749 if (auto acc
= findNntpAccountForFolder(fld)) {
750 fld.withBaseReader(delegate (abase
, cur
, top
, alist
) {
751 if (cur
< alist
.length
) {
752 if (auto art
= abase
[alist
[cur
]]) {
753 auto t
= fld.isTwittedNL(cur
);
754 auto setdg
= delegate (string name
, string mail
, string folder
, string title
) {
757 if (title
.length
!= 0) {
758 concmdfex
!"twit_set name \"%s\" mail \"%s\" folder_mask \"%s\" title \"%s\""((ConString cmd
) { xcmd
= cmd
.xstrip
.idup
; }, name
, mail
, folder
, title
);
761 if (title
.length
== 0) {
762 concmdfex
!"twit_unset name \"%s\" mail \"%s\" folder_mask \"%s\""((ConString cmd
) { xcmd
= cmd
.xstrip
.idup
; }, name
, mail
, folder
);
764 concmdfex
!"twit_set name \"%s\" mail \"%s\" folder_mask \"%s\" title \"%s\""((ConString cmd
) { xcmd
= cmd
.xstrip
.idup
; }, name
, mail
, folder
, title
);
768 import std
.path
: buildPath
;
769 auto fo
= VFile(buildPath(mailRootDir
, "auto_twits.rc"), "a");
771 } catch (Exception e
) {
772 conwriteln("ERROR writing twit info: ", e
.msg
);
776 auto tw
= new TitlerWindow(art
.fromname
, art
.frommail
, (acc
.inboxFolder
.startsWith("dmars_ng/") ?
"dmars_ng/*" : acc
.inboxFolder
), t
);
777 tw
.onSelected
= setdg
;
783 })("article_edit_poster_title", "edit poster's title of the current article");
786 // //////////////////////////////////////////////////////////////////// //
788 if (auto fld = getActiveFolder
) {
789 fld.withBaseReader((abase
, cur
, top
, alist
) {
790 if (cur
< alist
.length
) {
791 uint ridx
= alist
[cur
];
792 if (auto art
= abase
[ridx
]) {
793 while (abase
[ridx
].parent
!= 0) ridx
= abase
[ridx
].parent
;
794 twitThread(fld.folderPath
, art
.msgid
);
795 //TODO: mark thread articles as read
801 })("article_twit_thread", "twit current thread");
804 if (auto fld = getActiveFolder
) {
805 if (fld.toggleSoftDeleted()) postScreenRebuild();
807 })("article_softdelete_toggle", "toggle \"soft deleted\" flag on current article");
810 // //////////////////////////////////////////////////////////////////// //
812 if (auto fld = getActiveFolder
) {
813 if (fld.curidxValid
) {
814 fld.withBaseReader(delegate (abase
, cur
, top
, alist
) {
815 auto art
= abase
[alist
[cur
]];
817 conwriteln("============================");
818 abase
.loadContent(alist
[cur
]);
819 scope(exit
) art
.releaseContent();
821 foreach (ConString s
; art
.headersIterator
!false) {
823 if (from
.length
== 0 && s
.startsWithCI("From:")) from
= s
;
825 conwriteln("---------------");
826 conwriteln(" ", art
.fromname
, " <", art
.frommail
, ">");
827 conwriteln(" ", decodeq(from
));
830 foreach (char ch
; art
.fromname
) {
831 if (dc
.decode(cast(ubyte)ch
)) {
832 if (dc
.codepoint
> 127 || dc
.codepoint
< 32) conwritef
!" \\u%04X "(cast(uint)dc
.codepoint
);
833 else conwrite(cast(char)dc
.codepoint
);
841 })("article_dump_headers", "dump article headers");
844 if (auto fld = getActiveFolder
) {
848 })("folder_rebuild_index", "rebuild index file");
851 if (auto fld = getActiveFolder
) {
855 })("folder_pack_textdb", "pack (rebuild) text database for current folder");
857 conRegVar("folder_hide_old", "should old articles be hidden in UI?",
859 if (auto fld = getActiveFolder
) return fld.hideOldThreads
;
862 delegate (self
, bool nv
) {
863 if (auto fld = getActiveFolder
) fld.hideOldThreads
= nv
;
869 if (auto fld = getActiveFolder) {
870 fld.withBase(delegate (abase) {
871 uint idx = fld.curidx;
872 if (idx >= fld.length) {
874 } else if (auto art = abase[fld.baseidx(idx)]) {
875 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
876 idx = (idx+1)%fld.length;
879 foreach (immutable _; 0..fld.length) {
880 auto art = abase[fld.baseidx(idx)];
882 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
883 fld.curidx = cast(int)idx;
888 idx = (idx+1)%fld.length;
892 })("find_mine", "find mine article");
895 // //////////////////////////////////////////////////////////////////// //
898 })("dbg_font_window", "show window with font");
900 conRegVar
!dbg_dump_keynames("dbg_dump_key_names", "dump key names");
903 // //////////////////////////////////////////////////////////////////// //
904 conRegFunc
!((uint idx
, ConString fname
=null) {
905 import std
.format
: format
;
906 if (auto fld = getActiveFolder
) {
907 fld.withBaseReader(delegate (abase
, cur
, top
, alist
) {
908 if (cur
>= alist
.length
) return;
909 if (auto art
= abase
[alist
[cur
]]) {
910 abase
.loadContent(alist
[cur
]);
911 scope(exit
) art
.releaseContent();
913 art
.writeAttachmentToFile(idx
, delegate (ConString attname
) {
914 //import std.format : format;
915 import std
.file
: isDir
;
916 if (fname
.length
== 0) return "/tmp/"~Article
.fixAttachmentName(attname
);
918 string res
= fname
.idup
;
919 if (res
.length
&& res
[$-1] != '/') res
~= '/';
920 res
~= Article
.fixAttachmentName(attname
);
925 } catch (Exception e
) {
926 conwriteln("ERROR writing attachment");
931 { import core
.memory
: GC
; GC
.collect(); GC
.minimize(); }
932 })("attach_save", "save attach: attach_save index [filename]");
936 // ////////////////////////////////////////////////////////////////////////// //
937 //FIXME: turn into property
938 final class ArticleTextScrollEvent
{}
939 __gshared ArticleTextScrollEvent evArticleScroll
;
940 shared static this () {
941 evArticleScroll
= new ArticleTextScrollEvent();
944 void postArticleScroll () {
945 if (vbwin
!is null && !vbwin
.eventQueued
!ArticleTextScrollEvent
) vbwin
.postTimeout(evArticleScroll
, 25);
949 __gshared MainPaneWindow mainPane
;
952 final class MainPaneWindow
: SubWindow
{
953 string
[] emlines
; // in koi
954 uint emlinesBeforeAttaches
;
955 string lastDecodedMsgFolderPath
;
956 string lastDecodedMsgId
;
957 int articleTextTopLine
= 0;
958 int articleDestTextTopLine
= 0;
961 super(null, 0, 0, VBufWidth
, VBufHeight
);
962 mType
= Type
.OnBottom
;
966 // //////////////////////////////////////////////////////////////////// //
967 static struct WebLink
{
969 int x
, len
; // in pixels
971 string text
; // visual text
974 @property bool isAttach () const pure nothrow @safe @nogc { return (attachnum
>= 0); }
978 int lastUrlIndex
= -1;
980 void clearDecodedText () {
981 emurls
[] = WebLink
.init
;
984 emurls
.assumeSafeAppend
;
986 emlines
.assumeSafeAppend
;
987 lastDecodedMsgFolderPath
= null;
988 lastDecodedMsgId
= null;
989 articleTextTopLine
= 0;
990 articleDestTextTopLine
= 0;
995 int findUrlIndexAt (int mx
, int my
) {
996 int tpX0
= guiGroupListWidth
+2+1;
997 int tpX1
= VBufWidth
-1;
998 int tpY0
= guiThreadListHeight
+1;
999 int tpY1
= VBufHeight
-1;
1001 int y
= tpY0
+3*gxTextHeightUtf
+2;
1003 if (mx
< tpX0 || mx
> tpX1
) return -1;
1004 if (my
< y || my
> tpY1
) return -1;
1008 // yeah, i can easily calculate this, i know
1009 uint idx
= articleTextTopLine
;
1010 while (idx
< emlines
.length
&& y
< VBufHeight
) {
1011 if (my
>= y
&& my
< y
+gxTextHeightUtf
) {
1012 foreach (immutable uidx
, const ref WebLink wl
; emurls
) {
1014 if (mx
>= wl
.x
&& mx
< wl
.x
+wl
.len
) return cast(int)uidx
;
1019 y
+= gxTextHeightUtf
;
1025 WebLink
* findUrlAt (int mx
, int my
) {
1026 auto uidx
= findUrlIndexAt(mx
, my
);
1027 return (uidx
>= 0 ?
&emurls
[uidx
] : null);
1030 private void emlDetectUrls (uint textlines
) {
1031 static immutable string
[3] protos
= [ "https", "http", "ftp" ];
1035 static sptrdiff
urlepos (const(char)[] s
, sptrdiff spos
) {
1036 assert(spos
< s
.length
);
1037 spos
= s
.indexOf("://", spos
);
1041 while (spos
< s
.length
) {
1043 if (ch
== '/') break;
1044 if (ch
<= ' ') return spos
;
1045 if (ch
!= '.' && ch
!= '-' && !ch
.isalnum
) return spos
;
1048 if (spos
>= s
.length
) return spos
;
1050 assert(s
[spos
] == '/');
1051 while (spos
< s
.length
) {
1053 if (ch
<= ' ' || ch
== '<' || ch
== '>') return spos
;
1054 if (ch
== '.' || ch
== ')' || ch
== '-' || ch
== '!' || ch
== ';') {
1055 if (s
.length
-spos
> 1) {
1057 if (ch
<= ' ' || ch
== '.' || ch
== '(' || ch
== ')' || ch
== '-' || ch
== '!' || ch
== ';' || ch
== '<' || ch
== '>') return spos
;
1065 if (textlines
> emlines
.length
) textlines
= cast(uint)emlines
.length
; // just in case
1066 foreach (immutable cy
, string s
; emlines
[0..textlines
]) {
1067 if (s
.length
== 0) continue;
1068 auto pos
= s
.indexOf("://");
1072 foreach (string proto
; protos
) {
1073 if (spos
>= proto
.length
&& proto
.strEquCI(s
[spos
-proto
.length
..spos
])) {
1074 if (spos
== proto
.length ||
!s
[spos
-proto
.length
-1].isalnum
) {
1076 spos
-= proto
.length
;
1083 auto epos
= urlepos(s
, spos
);
1084 //conwriteln("spos=", spos, "; epos=", epos, "; s=[", s, "]; link=[", s[spos..epos], "]");
1086 wl
.ly
= cast(int)cy
;
1087 wl
.x
= gxTextWidthP(s
[0..spos
]);
1088 wl
.len
= gxTextWidthP(s
[spos
..epos
]);
1089 wl
.url
= wl
.text
= s
[spos
..epos
];
1090 if (spos
> 0) ++wl
.x
;
1096 //conwriteln("pos=", pos, "; s=[", s, "]; rest=[", s[pos..$], "]");
1097 pos
= s
.indexOf("://", pos
);
1101 int attachcount
= 0;
1102 foreach (immutable uint cy
; textlines
..cast(uint)emlines
.length
) {
1103 string s
= emlines
[cy
];
1104 if (s
.length
== 0) continue;
1105 auto spos
= s
.indexOf("attach:");
1106 if (spos
< 0) continue;
1108 while (epos
< s
.length
&& s
.ptr
[epos
] > ' ') ++epos
;
1109 //if (attachcount >= parts.length) break;
1111 wl
.ly
= cast(int)cy
;
1112 wl
.x
= gxTextWidthP(s
[0..spos
]);
1113 wl
.len
= gxTextWidthP(s
[spos
..epos
]);
1114 wl
.url
= wl
.text
= s
[spos
..epos
];
1115 if (spos
> 0) ++wl
.x
;
1116 wl
.attachnum
= attachcount
;
1117 //wl.attachfname = s[spos+7..epos];
1118 //wl.part = parts[attachcount];
1124 bool needToDecodeArticleTextNL (Folder
fld, Article art
) nothrow @trusted @nogc {
1125 if (art
is null) return false;
1126 if (lastDecodedMsgId
!= art
.msgid
) return true;
1127 return (fld !is null ? lastDecodedMsgFolderPath
!= fld.folderPath
: lastDecodedMsgFolderPath
.length
!= 0);
1130 // fld is locked here
1131 void decodeArticleTextNL(bool doLocalConvert
=true) (Folder
fld, Article art
) {
1132 static if (doLocalConvert
) {
1133 if (art
is null) { clearDecodedText(); return; }
1134 if (!needToDecodeArticleTextNL(fld, art
)) return;
1138 emlines
~= null; // hack; this dummy line will be removed
1139 lastDecodedMsgFolderPath
= (fld !is null ?
fld.folderPath
: null);
1140 lastDecodedMsgId
= art
.msgid
;
1142 bool lastEndsWithSpace () { return (emlines
[$-1].length ? emlines
[$-1][$-1] == ' ' : false); }
1146 void putLine (ConString s
) {
1147 enum QuoteStr
= ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ";
1148 static string
addQuotes(T
:const(char)[]) (T s
, int qlevel
) {
1150 static if (is(T
== string
) ||
is (T
== typeof(null))) return s
; else return s
.idup
;
1152 if (qlevel
> QuoteStr
.length
-1) qlevel
= cast(int)QuoteStr
.length
-1;
1153 return QuoteStr
[$-qlevel
-1..$]~s
;
1155 // calculate quote level
1157 if (s
.length
&& s
[0] == '>') {
1158 usize lastqpos
= 0, pos
= 0;
1159 while (pos
< s
.length
) {
1160 if (s
.ptr
[pos
] != '>') {
1161 if (s
.ptr
[pos
] != ' ') break;
1168 if (s
.length
-lastqpos
> 1 && s
.ptr
[lastqpos
+1] == ' ') ++lastqpos
;
1172 //conwriteln("qlevel=", qlevel, "; s=[", s, "]");
1173 // empty line: just insert it
1174 if (s
.length
== 0) {
1175 emlines
~= addQuotes(null, qlevel
).xstripright
;
1178 bool newline
= false;
1179 if (lastQLevel
!= qlevel ||
!lastEndsWithSpace
) {
1183 //while (s.length > 1 && s.ptr[0] <= ' ') s = s[1..$];
1188 while (epos
< s
.length
&& s
.ptr
[epos
] <= ' ') ++epos
;
1190 //assert(s[0] > ' ');
1191 while (epos
< s
.length
&& s
.ptr
[epos
] <= ' ') ++epos
;
1193 while (epos
< s
.length
&& s
.ptr
[epos
] > ' ') ++epos
;
1195 while (epos
< s
.length
&& s
.ptr
[epos
] <= ' ') ++epos
;
1196 if (!newline
&& emlines
[$-1].length
+xlen
<= 80) {
1197 // no wrapping, continue last line
1198 emlines
[$-1] ~= s
[0..epos
];
1201 // wrapping; new line
1202 emlines
~= addQuotes(s
[0..epos
], qlevel
);
1206 if (newline
) emlines
~= addQuotes(null, qlevel
);
1208 lastQLevel
= qlevel
;
1212 foreach (ConString s
; LineIterator
!false(art
.getTextContent
)) {
1213 static if (doLocalConvert
) {
1214 putLine(s
.uniRecode
);
1220 // remove first dummy line
1221 if (emlines
.length
) emlines
= emlines
[1..$];
1222 // remove trailing empty lines
1223 while (emlines
.length
&& emlines
[$-1].xstrip
.length
== 0) emlines
.length
-= 1;
1224 } catch (Exception e
) {
1225 conwriteln("================================= ERROR: ", e
.msg
, " =================================");
1226 conwriteln(e
.toString
);
1230 auto lcount
= cast(uint)emlines
.length
;
1231 emlinesBeforeAttaches
= lcount
;
1233 static if (doLocalConvert
) {
1235 art
.forEachAttachment(delegate(ConString type
, ConString filename
) {
1236 if (attcount
== 0) { emlines
~= null; emlines
~= null; }
1237 import std
.format
: format
;
1238 if (type
.length
== 0) type
= "unknown/unknown";
1239 string s
= " [%02s] attach:%s -- %s".format(attcount
, Article
.fixAttachmentName(filename
), type
);
1244 emlDetectUrls(lcount
);
1246 emlDetectUrls(lcount
);
1250 @property int visibleArticleLines () {
1251 int y
= guiThreadListHeight
+1+3*gxTextHeightUtf
+2;
1252 return (VBufHeight
-y
)/gxTextHeightUtf
;
1255 void normalizeArticleTopLine () {
1256 int lines
= visibleArticleLines
;
1257 if (lines
< 1 || emlines
.length
<= lines
) {
1258 articleTextTopLine
= 0;
1259 articleDestTextTopLine
= 0;
1261 if (articleTextTopLine
< 0) articleTextTopLine
= 0;
1262 if (articleTextTopLine
+lines
> emlines
.length
) {
1263 articleTextTopLine
= cast(int)emlines
.length
-lines
;
1264 if (articleTextTopLine
< 0) articleTextTopLine
= 0;
1269 void doScrollStep () {
1270 auto oldtop
= articleTextTopLine
;
1272 normalizeArticleTopLine();
1273 if (articleDestTextTopLine
< articleTextTopLine
) {
1274 --articleTextTopLine
;
1275 } else if (articleDestTextTopLine
> articleTextTopLine
) {
1276 ++articleTextTopLine
;
1280 normalizeArticleTopLine();
1282 if (articleTextTopLine
== oldtop
) {
1283 // can't scroll anymore
1284 articleDestTextTopLine
= articleTextTopLine
;
1287 postScreenRebuild();
1288 postArticleScroll();
1291 void scrollBy (int delta
) {
1292 articleDestTextTopLine
+= delta
;
1296 void scrollByPageUp () {
1297 int lines
= visibleArticleLines
-1;
1298 if (lines
< 1) lines
= 1;
1302 void scrollByPageDown () {
1303 int lines
= visibleArticleLines
-1;
1304 if (lines
< 1) lines
= 1;
1308 // //////////////////////////////////////////////////////////////////// //
1309 //TODO: move parts to widgets
1310 override void onPaint () {
1313 gxFillRect(0, 0, guiGroupListWidth
, VBufHeight
, gxRGB
!(20, 20, 20));
1314 gxVLine(guiGroupListWidth
, 0, VBufHeight
, gxRGB
!(255, 255, 255));
1316 gxFillRect(guiGroupListWidth
+1, 0, VBufWidth
, guiThreadListHeight
, gxRGB
!(15, 15, 15));
1317 gxHLine(guiGroupListWidth
+1, guiThreadListHeight
, VBufWidth
, gxRGB
!(255, 255, 255));
1319 // called with locked folder
1320 void drawArticle (ArticleBase abase
, Folder
fld, Article art
, uint cidx
, uint aidx
) {
1321 import core
.stdc
.stdio
: snprintf
;
1322 import std
.format
: format
;
1323 import std
.datetime
;
1325 const(char)[] tbufs
;
1327 void xfmt (string s
, const(char)[][] strs
...) {
1329 void puts (const(char)[] s
...) {
1330 foreach (char ch
; s
) {
1331 if (dpos
>= tbuf
.length
) break;
1336 if (strs
.length
&& s
.length
> 1 && s
[0] == '%' && s
[1] == 's') {
1345 tbufs
= tbuf
[0..dpos
];
1348 if (needToDecodeArticleTextNL(fld, art
)) {
1349 abase
.loadContent(aidx
);
1350 scope(exit
) abase
.releaseContent(aidx
);
1351 assert(art
.contentLoaded
);
1352 decodeArticleTextNL(fld, art
);
1356 clipX0 = guiGroupListWidth+2;
1357 clipX1 = VBufWidth-1;
1358 clipY0 = guiThreadListHeight+1;
1359 clipY1 = VBufHeight-1;
1361 clipRect
= GxRect(GxPoint(guiGroupListWidth
+2, guiThreadListHeight
+1), GxPoint(VBufWidth
-1, VBufHeight
-1));
1363 int msx
= lastMouseX
;
1364 int msy
= lastMouseY
;
1367 gxFillRect(clipRect
.x0
, clipRect
.y0
, clipRect
.x1
-clipRect
.x0
+1, 3*gxTextHeightUtf
+2, gxRGB
!(30, 30, 30));
1368 //gxDrawTextUtf(clipX0+1, clipY0+0*gxTextHeightUtf+1, "From: %s <%s>".format(art.fromname, art.frommail), gxRGB!(0, 128, 128));
1369 //gxDrawTextUtf(clipX0+1, clipY0+1*gxTextHeightUtf+1, "Subject: %s".format(art.subj), gxRGB!(0, 128, 128));
1370 xfmt("From: %s <%s>", art
.fromname
, art
.frommail
);
1371 gxDrawTextUtf(clipRect
.x0
+1, clipRect
.y0
+0*gxTextHeightUtf
+1, tbufs
, gxRGB
!(0, 128, 128));
1372 xfmt("Subject: %s", art
.subj
);
1373 gxDrawTextUtf(clipRect
.x0
+1, clipRect
.y0
+1*gxTextHeightUtf
+1, tbufs
, gxRGB
!(0, 128, 128));
1375 auto t
= SysTime
.fromUnixTime(art
.time
);
1376 //string s = "Date: %04d/%02d/%02d %02d:%02d:%02d".format(t.year, t.month, t.day, t.hour, t.minute, t.second);
1377 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
);
1378 gxDrawTextUtf(clipRect
.x0
+1, clipRect
.y0
+2*gxTextHeightUtf
+1, tbuf
[0..tlen
], gxRGB
!(0, 128, 128));
1382 int y
= clipRect
.y0
+3*gxTextHeightUtf
+2;
1385 normalizeArticleTopLine();
1387 bool drawUpMark
= (articleTextTopLine
> 0);
1388 bool drawDownMark
= false;
1390 uint idx
= articleTextTopLine
;
1391 bool msvisible
= isMouseVisible
;
1392 while (idx
< emlines
.length
&& y
< VBufHeight
) {
1394 string s
= emlines
[idx
];
1395 uint clr
= gxRGB
!(0, 128, 0);
1397 foreach (char ch
; s
) {
1398 if (ch
<= ' ') continue;
1399 if (ch
!= '>') break;
1403 clr
= gxRGB
!( 0, 128+40, 0);
1405 final switch (qlevel
%2) {
1406 case 0: clr
= gxRGB
!(128, 128, 0); break;
1407 case 1: clr
= gxRGB
!( 0, 128, 128); break;
1411 gxDrawTextP
!4(clipRect
.x0
+1, y
, s
, clr
);
1413 foreach (const ref WebLink wl
; emurls
) {
1415 uint lclr
= gxRGB
!(0, 200, 200);
1416 if (msvisible
&& msy
>= y
&& msy
< y
+gxTextHeightUtf
&& msx
>= clipRect
.x0
+1+wl
.x
&& msx
< clipRect
.x0
+1+wl
.x
+wl
.len
) {
1417 lclr
= (lastMouseLeft ? gxRGB
!(255, 0, 255) : gxRGB
!(0, 255, 255));
1419 gxDrawTextP
!4(clipRect
.x0
+1+wl
.x
, y
, wl
.text
, lclr
);
1423 if (clipRect
.y1
-y
< gxTextHeightUtf
&& emlines
.length
-idx
> 0) drawDownMark
= true;
1426 y
+= gxTextHeightUtf
;
1429 gxDrawTextOutP(clipRect
.x1
-gxTextWidthP(triangleDownStr
)-3, sty
, triangleDownStr
, (drawUpMark ? gxRGB
!(255, 255, 255) : gxRGB
!(85, 85, 85)), gxRGB
!(0, 69, 69));
1430 gxDrawTextOutP(clipRect
.x1
-gxTextWidthP(triangleUpStr
)-3, clipRect
.y1
-7, triangleUpStr
, (drawDownMark ? gxRGB
!(255, 255, 255) : gxRGB
!(85, 85, 85)), gxRGB
!(0, 69, 69));
1432 gxDrawScrollBar(GxRect(clipRect
.x1
-10, sty
+10, 5, clipRect
.y1
-sty
-17), cast(int)emlines
.length
-1, idx
-1);
1434 string twittext
= fld.isTwittedNL(cidx
);
1435 if (twittext
!is null) {
1436 foreach (immutable dy
; clipRect
.y0
+3*gxTextHeightUtf
+2..clipRect
.y1
+1) {
1437 foreach (immutable dx
; clipRect
.x0
..clipRect
.x1
+1) {
1438 if ((dx^dy
)&1) gxPutPixel(dx
, dy
, gxRGBA
!(0, 0, 80, 127));
1442 if (twittext
.length
) {
1443 int tx
= clipRect
.x0
+(clipRect
.width
-gxTextWidthScaledUtf(3, twittext
))/2-1;
1444 int ty
= clipRect
.y0
+(clipRect
.height
-3*gxTextHeightUtf
)/2-1;
1445 foreach (immutable dy
; -1..2) {
1446 foreach (immutable dx
; -1..2) {
1447 if (dx || dy
) gxDrawTextScaledUtf(3, tx
+dx
, ty
+dy
, twittext
, 0);
1450 gxDrawTextScaledUtf(3, tx
, ty
, twittext
, gxRGB
!(255, 0, 0));
1455 void drawThreadList (Folder
fld) {
1456 if (fld.needRebuild
) fld.buildVisibleList();
1457 fld.makeCurrentVisible();
1458 fld.withBaseReader(delegate (abase
, cur
, top
, alist
) {
1459 if (alist
.length
== 0) return;
1461 clipRect
.x0
= guiGroupListWidth
+2;
1462 clipRect
.x1
= VBufWidth
-1-5;
1464 clipRect
.y1
= guiThreadListHeight
-1;
1465 immutable uint origX0
= clipRect
.x0
;
1466 immutable uint origX1
= clipRect
.x1
;
1467 immutable uint origY0
= clipRect
.y0
;
1468 immutable uint origY1
= clipRect
.y1
;
1470 //conwriteln(fld.msgtop, " : ", fld.list.length);
1472 while (idx
< alist
.length
&& y
< guiThreadListHeight
) {
1473 import std
.format
: format
;
1474 import std
.datetime
;
1476 if (y
>= guiThreadListHeight
) break;
1477 if (idx
>= alist
.length
) break;
1479 clipRect
.x0
= origX0
;
1480 clipRect
.x1
= origX1
;
1482 auto art
= abase
[alist
[idx
]];
1484 //conwriteln(idx, " : ", fld.list.length);
1486 uint cc
= gxRGB
!(0, 127, 127);
1487 if (art
.softDeleted
) cc
= gxRGB
!(0, 127-30, 127-30);
1488 gxFillRect(clipRect
.x0
, y
, clipRect
.width
-1, gxTextHeightUtf
, cc
);
1490 clipRect
.x0
= clipRect
.x0
+1;
1491 clipRect
.x1
= clipRect
.x1
-1;
1493 //uint clr = (idx != fld.curidx ? gxRGB!(255, 127, 0) : gxRGB!(255, 255, 255));
1494 uint clr
= (art
.unread ? gxRGB
!(255, 255, 255) : gxRGB
!(255-60, 127-60, 0));
1495 uint clr1
= (art
.unread ? gxRGB
!(255, 255, 0) : gxRGB
!(255-60-40, 127-60-40, 0));
1497 if (art
.depth
!= 0 && !art
.unread
) {
1498 clr
= gxRGB
!(255-90, 127-90, 0);
1499 clr1
= gxRGB
!(255-90-40, 127-90-40, 0);
1502 if (!art
.unread
&& !art
.softDeleted
) {
1503 if (fld.isTwittedNL(idx
) !is null) { clr
= gxRGB
!(60, 0, 0); clr1
= gxRGB
!(60, 0, 0); }
1504 if (fld.isHighlightedNL(idx
)) { clr
= gxRGB
!(0, 190, 0); clr1
= gxRGB
!(0, 190-40, 0); }
1505 } else if (art
.softDeleted
) {
1506 //clr = gxRGB!(255-80, 127-80, 0);
1507 //clr1 = gxRGB!(255-80-40, 127-80-40, 0);
1508 clr
= clr1
= gxRGB
!(127, 0, 0);
1511 auto t
= SysTime
.fromUnixTime(art
.time
);
1512 //string s = "%04d/%02d/%02d %02d:%02d".format(t.year, t.month, t.day, t.hour, t.minute);
1514 import core
.stdc
.stdio
: snprintf
;
1516 //string s = "%02d/%02d %02d:%02d".format(t.month, t.day, t.hour, t.minute);
1517 auto len
= snprintf(tmpbuf
.ptr
, tmpbuf
.length
, "%02d/%02d/%02d %02d:%02d", t
.year
%100, t
.month
, t
.day
, t
.hour
, t
.minute
);
1518 gxDrawTextUtf(clipRect
.x1
-gxTextWidthUtf(tmpbuf
[0..len
]), y
, tmpbuf
[0..len
], clr
);
1521 string from
= art
.fromname
;
1523 auto vp
= from
.indexOf(" via Digitalmars-");
1525 from
= from
[0..vp
].xstrip
;
1526 if (from
.length
== 0) from
= "anonymous";
1530 clipRect
.x1
= clipRect
.x1
-(13*6+4);
1531 gxDrawTextUtf(clipRect
.x1
-22*6, y
, from
, clr
);
1532 gxDrawTextUtf(clipRect
.x1
-22*6+gxTextWidthUtf(from
)+4, y
, "<", clr1
);
1533 gxDrawTextUtf(clipRect
.x1
-22*6+gxTextWidthUtf(from
)+4+gxTextWidthUtf("<")+1, y
, art
.frommail
, clr1
);
1534 gxDrawTextUtf(clipRect
.x1
-22*6+gxTextWidthUtf(from
)+4+gxTextWidthUtf("<")+1+gxTextWidthUtf(art
.frommail
)+1, y
, ">", clr1
);
1536 clipRect
.x1
= clipRect
.x1
-(22*6+4);
1537 gxDrawTextUtf(clipRect
.x0
+art
.depth
*3, y
, art
.subj
, clr
);
1538 foreach (immutable dx
; 0..art
.depth
) gxPutPixel(clipRect
.x0
+1+dx
*3, y
+gxTextHeightUtf
/2, gxRGB
!(70, 70, 70));
1540 if (art
.softDeleted
) {
1541 clipRect
.x0
= origX0
;
1542 clipRect
.x1
= origX1
;
1543 gxHLine(clipRect
.x0
, y
+gxTextHeightUtf
/2, clipRect
.x1
-clipRect
.x0
+1, clr
);
1547 y
+= gxTextHeightUtf
;
1552 clipRect
.x0
= origX0
;
1553 clipRect
.x1
= origX1
+5;
1554 clipRect
.y0
= origY0
;
1555 clipRect
.y1
= origY1
;
1556 gxDrawScrollBar(GxRect(clipRect
.x1
-5, clipRect
.y0
, 4, clipRect
.height
-1), cast(int)alist
.length
-1, idx
-1);
1559 if (cur
< alist
.length
) drawArticle(abase
, fld, abase
[alist
[cur
]], cur
, alist
[cur
]);
1564 folderMakeCurVisible();
1567 foreach (immutable idx
, Folder
fld; folders
) {
1568 if (idx
< folderTop
) continue;
1569 if (ofsy
>= VBufHeight
) break;
1571 if (idx
== folderCur
) gxFillRect(0, ofsy
-1, guiGroupListWidth
, (gxTextHeightUtf
+2), gxRGB
!(0, 127, 127));
1572 clipRect
.x0
= ofsx
-1;
1574 clipRect
.x1
= guiGroupListWidth
-3;
1575 int depth
= folderDepth(idx
);
1576 uint clr
= gxRGB
!(255-30, 127-30, 0);
1577 if (fld.unreadCount
) {
1578 clr
= gxRGB
!(0, 255, 255);
1581 clr
= (fld.folderPath
== "accounts" ? gxRGB
!(220, 220, 0) : gxRGB
!(255, 127+60, 0));
1582 } else if (fld.folderPath
.startsWith("accounts/") && fld.folderPath
.endsWith("/inbox")) {
1583 clr
= gxRGB
!(255, 127+30, 0);
1586 foreach (immutable dd; 0..depth
) gxPutPixel(ofsx
+dd*6+2, ofsy
+gxTextHeightUtf
/2, gxRGB
!(80, 80, 80));
1587 gxDrawTextOutP(ofsx
+depth
*6, ofsy
, folderVisName(idx
), clr
, gxRGB
!(0, 0, 0));
1588 ofsy
+= gxTextHeightUtf
+2;
1592 if (folderCur
< folders
.length
) drawThreadList(folders
[folderCur
]);
1595 override bool onKey (KeyEvent event
) {
1596 if (event
.pressed
) {
1597 if (dbg_dump_keynames
) conwriteln("key: ", event
.toStr
, ": ", event
.modifierState
&ModifierState
.windows
);
1598 //foreach (const ref kv; mainAppKeyBindings.byKeyValue) if (event == kv.key) concmd(kv.value);
1600 if (auto cmdp
= event
.toStrBuf(kname
[]) in mainAppKeyBindings
) {
1606 if (event == "S-Up") {
1607 if (folderTop > 0) --folderTop;
1608 postScreenRebuild();
1611 if (event == "S-Down") {
1612 if (folderTop+1 < folders.length) ++folderTop;
1613 postScreenRebuild();
1617 //if (event == "Tab") { new PostWindow(); return true; }
1618 //if (event == "Tab") { new SelectPopBoxWindow(null); return true; }
1623 // returning `false` to avoid screen rebuilding by dispatcher
1624 override bool onMouse (MouseEvent event
) {
1625 //FIXME: use window coordinates
1627 event
.mouse2xy(mx
, my
);
1629 if (event
.type
== MouseEventType
.buttonPressed
&& event
.button
== MouseButton
.left
) {
1631 if (mx
>= 0 && mx
< guiGroupListWidth
&& my
>= 0 && my
< VBufHeight
) {
1632 uint fnum
= my
/(gxTextHeightUtf
+2)+folderTop
;
1633 if (fnum
!= folderCur
) {
1635 postScreenRebuild();
1640 if (mx
> guiGroupListWidth
&& mx
< VBufWidth
&& my
>= 0 && my
< guiThreadListHeight
) {
1641 if (auto fld = getActiveFolder
) {
1642 my
/= gxTextHeightUtf
;
1643 fld.curidx
= fld.msgtop
+my
;
1644 postScreenRebuild();
1650 if (event
.type
== MouseEventType
.buttonPressed
&& (event
.button
== MouseButton
.wheelUp || event
.button
== MouseButton
.wheelDown
)) {
1652 if (mx
>= 0 && mx
< guiGroupListWidth
&& my
>= 0 && my
< VBufHeight
) {
1653 if (event
.button
== MouseButton
.wheelUp
) {
1654 if (folderCur
> 0) folderCur
= folderCur
-1;
1656 if (folderCur
+1 < folders
.length
) folderCur
= folderCur
+1;
1658 postScreenRebuild();
1662 if (mx
> guiGroupListWidth
&& mx
< VBufWidth
&& my
>= 0 && my
< guiThreadListHeight
) {
1663 if (auto fld = getActiveFolder
) {
1664 if (event
.button
== MouseButton
.wheelUp
) fld.moveUp(); else fld.moveDown();
1665 postScreenRebuild();
1670 if (mx
> guiGroupListWidth
&& mx
< VBufWidth
&& my
> guiThreadListHeight
&& my
< VBufHeight
) {
1671 enum ScrollLines
= 2;
1672 if (event
.button
== MouseButton
.wheelUp
) scrollBy(-ScrollLines
); else scrollBy(ScrollLines
);
1673 postScreenRebuild();
1678 if (event
.type
== MouseEventType
.buttonReleased
&& event
.button
== MouseButton
.left
) {
1680 auto url
= findUrlAt(mx
, my
);
1683 concmdf
!"attach_save %s"(url
.attachnum
);
1685 concmdf
!"open_url \"%s\""(url
.url
);
1689 if (event
.type
== MouseEventType
.motion
) {
1690 auto uidx
= findUrlIndexAt(mx
, my
);
1691 if (uidx
!= lastUrlIndex
) { lastUrlIndex
= uidx
; postScreenRebuild(); return false; }
1694 if (event
.type
== MouseEventType
.buttonPressed || event
.type
== MouseEventType
.buttonReleased
) {
1695 postScreenRebuild();
1697 postScreenRepaint();
1703 override bool onChar (dchar ch
) {
1709 // ////////////////////////////////////////////////////////////////////////// //
1710 void repaintScreen () {
1718 // ////////////////////////////////////////////////////////////////////////// //
1719 class FontWindow
: SubWindow
{
1723 super("", 16*14+4, 16*14+4+10);
1724 //if (hasWindowClass(this)) return;
1728 override void onPaint () {
1729 import core
.stdc
.stdio
: snprintf
;
1732 auto tlen
= snprintf(buf
.ptr
, buf
.length
, "Font: \\x%02x (%d)", cast(int)(cy
*16+cx
), cast(int)(cy
*16+cx
));
1735 gxDrawWindow(buf
[0..tlen
], gxRGB
!(255, 255, 255), gxRGB
!(0, 0, 0), gxRGB
!(255, 255, 255), gxRGB
!(0, 0, 180));
1740 foreach (immutable int dy
; 0..16) {
1741 foreach (immutable int dx
; 0..16) {
1742 if (cx
== dx
&& cy
== dy
) gxFillRect(clipRect
.x0
+dx
*14, clipRect
.y0
+dy
*14, 12, 13, 0);
1743 gxDrawCharP(clipRect
.x0
+dx
*14+2, clipRect
.y0
+dy
*14+2, cast(char)(dy
*16+dx
),
1744 ((dx^dy
)&1 ? gxRGB
!(255, 255, 255) : gxRGB
!(255, 255, 0)));
1749 override bool onKey (KeyEvent event
) {
1750 if (event
.pressed
) {
1751 if (event
== "Escape" || event
== "C-Q") { close(); return true; }
1752 if (event
== "Left") { cx
= (cx
+15)%16; return true; }
1753 if (event
== "Right") { cx
= (cx
+1)%16; return true; }
1754 if (event
== "Up") { cy
= (cy
+15)%16; return true; }
1755 if (event
== "Down") { cy
= (cy
+1)%16; return true; }
1756 if (event
== "Home") { cx
= 0; return true; }
1757 if (event
== "End") { cx
= 15; return true; }
1758 if (event
== "PageUp") { cy
= 0; return true; }
1759 if (event
== "PageDown") { cy
= 15; return true; }
1761 return super.onKey(event
);
1766 // ////////////////////////////////////////////////////////////////////////// //
1767 __gshared LockFile mainLockFile
;
1770 void checkMainLockFile () {
1771 import std
.path
: buildPath
;
1772 mainLockFile
= LockFile(buildPath(mailRootDir
, ".chiroptera.lock"));
1773 if (!mainLockFile
.tryLock
) { mainLockFile
.close(); assert(0, "already running"); }
1777 void main (string
[] args
) {
1778 checkMainLockFile();
1779 scope(exit
) mainLockFile
.close();
1781 sdpyWindowClass
= "Chiroptera";
1782 glconShowKey
= "M-Grave";
1785 hitwitInitConsole();
1788 setupDefaultBindings();
1790 concmd("exec chiroptera.rc tan");
1794 concmdf
!"exec %s/accounts.rc tan"(mailRootDir
);
1795 concmdf
!"exec %s/addressbook.rc tan"(mailRootDir
);
1796 concmdf
!"exec %s/filters.rc tan"(mailRootDir
);
1797 concmdf
!"exec %s/highlights.rc tan"(mailRootDir
);
1798 concmdf
!"exec %s/twits.rc tan"(mailRootDir
);
1799 concmdf
!"exec %s/twit_threads.rc tan"(mailRootDir
);
1800 concmdf
!"exec %s/auto_twits.rc tan"(mailRootDir
);
1801 concmdf
!"exec %s/auto_twit_threads.rc tan"(mailRootDir
);
1802 conProcessQueue(); // load config
1803 conProcessArgs
!true(args
);
1805 vbufEffScale
= VBufScale
;
1806 vbufEffVSync
= vbufVSync
;
1808 lastWinWidth
= winWidthScaled
;
1809 lastWinHeight
= winHeightScaled
;
1811 restoreCurrentFolderAndPosition();
1813 vbwin
= new SimpleWindow(lastWinWidth
, lastWinHeight
, "Chiroptera", OpenGlOptions
.yes
, Resizablity
.allowResizing
);
1816 vbwin
.onFocusChange
= delegate (bool focused
) {
1817 vbfocused
= focused
;
1819 lastMouseButton
= 0;
1820 eguiLostGlobalFocus();
1824 vbwin
.windowResized
= delegate (int wdt
, int hgt
) {
1825 // TODO: fix gui sizes
1826 if (vbwin
.closed
) return;
1828 if (lastWinWidth
== wdt
&& lastWinHeight
== hgt
) return;
1829 glconResize(wdt
, hgt
);
1831 double glwFrac
= cast(double)guiGroupListWidth
/VBufWidth
;
1832 double tlhFrac
= cast(double)guiThreadListHeight
/VBufHeight
;
1834 vbufEffScale
= VBufScale
;
1835 if (wdt
< VBufScale
*32) wdt
= VBufScale
;
1836 if (hgt
< VBufScale
*32) hgt
= VBufScale
;
1837 VBufWidth
= (wdt
+VBufScale
-1)/VBufScale
;
1838 VBufHeight
= (hgt
+VBufScale
-1)/VBufScale
;
1840 import core
.stdc
.stdlib
: realloc
;
1841 auto nv
= cast(uint*)realloc(vglTexBuf
, (VBufWidth
*VBufHeight
+4)*4);
1842 if (nv
is null) assert(0, "out of memory!");
1846 guiGroupListWidth
= cast(int)(glwFrac
*VBufWidth
+0.5);
1847 guiThreadListHeight
= cast(int)(tlhFrac
*VBufHeight
+0.5);
1849 if (guiGroupListWidth
< 12) guiGroupListWidth
= 12;
1850 if (guiThreadListHeight
< 16) guiThreadListHeight
= 16;
1853 lastWinHeight
= hgt
;
1855 // reinitialize OpenGL texture
1859 enum wrapOpt
= GL_REPEAT
;
1860 enum filterOpt
= GL_NEAREST
; //GL_LINEAR;
1861 enum ttype
= GL_UNSIGNED_BYTE
;
1863 if (vglTexId
) glDeleteTextures(1, &vglTexId
);
1865 glGenTextures(1, &vglTexId
);
1866 if (vglTexId
== 0) assert(0, "can't create OpenGL texture");
1868 //GLint gltextbinding;
1869 //glGetIntegerv(GL_TEXTURE_BINDING_2D, &gltextbinding);
1870 //scope(exit) glBindTexture(GL_TEXTURE_2D, gltextbinding);
1872 glBindTexture(GL_TEXTURE_2D
, vglTexId
);
1873 glTexParameterf(GL_TEXTURE_2D
, GL_TEXTURE_WRAP_S
, wrapOpt
);
1874 glTexParameterf(GL_TEXTURE_2D
, GL_TEXTURE_WRAP_T
, wrapOpt
);
1875 glTexParameteri(GL_TEXTURE_2D
, GL_TEXTURE_MIN_FILTER
, filterOpt
);
1876 glTexParameteri(GL_TEXTURE_2D
, GL_TEXTURE_MAG_FILTER
, filterOpt
);
1877 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
1878 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
1880 GLfloat
[4] bclr
= 0.0;
1881 glTexParameterfv(GL_TEXTURE_2D
, GL_TEXTURE_BORDER_COLOR
, bclr
.ptr
);
1883 glTexImage2D(GL_TEXTURE_2D
, 0, GL_RGBA
, VBufWidth
, VBufHeight
, 0, GLTexType
, GL_UNSIGNED_BYTE
, vglTexBuf
);
1889 vbwin
.redrawOpenGlSceneNow();
1892 vbwin
.addEventListener((DoConsoleCommandsEvent evt
) {
1893 bool sendAnother
= false;
1894 bool prevVisible
= isConsoleVisible
;
1897 scope(exit
) consoleUnlock();
1899 sendAnother
= !conQueueEmpty();
1901 if (sendAnother
) postDoConCommands();
1902 if (vbwin
.closed
) return;
1903 if (isQuitRequested
) { vbwin
.close(); return; }
1904 if (prevVisible || isConsoleVisible
) postScreenRepaintDelayed();
1907 vbwin
.addEventListener((HideMouseEvent evt
) {
1908 if (vbwin
.closed
) return;
1909 if (isQuitRequested
) { vbwin
.close(); return; }
1910 if (!repostHideMouse
) {
1911 if (mainPane
!is null) mainPane
.lastUrlIndex
= -1;
1913 vbwin
.redrawOpenGlSceneNow();
1917 vbwin
.addEventListener((ScreenRebuildEvent evt
) {
1918 if (vbwin
.closed
) return;
1919 if (isQuitRequested
) { vbwin
.close(); return; }
1921 vbwin
.redrawOpenGlSceneNow();
1922 if (isConsoleVisible
) postScreenRepaintDelayed();
1925 vbwin
.addEventListener((ScreenRepaintEvent evt
) {
1926 if (vbwin
.closed
) return;
1927 if (isQuitRequested
) { vbwin
.close(); return; }
1928 vbwin
.redrawOpenGlSceneNow();
1929 if (isConsoleVisible
) postScreenRepaintDelayed();
1932 vbwin
.addEventListener((CursorBlinkEvent evt
) {
1933 if (vbwin
.closed
) return;
1935 vbwin
.redrawOpenGlSceneNow();
1938 vbwin
.addEventListener((QuitEvent evt
) {
1939 if (vbwin
.closed
) return;
1940 if (isQuitRequested
) { vbwin
.close(); return; }
1945 vbwin
.addEventListener((UnreadChangedEvent evt
) {
1946 if (vbwin
.closed
) return;
1947 if (isQuitRequested
) { vbwin
.close(); return; }
1948 setupTrayAnimation();
1951 vbwin
.addEventListener((TrayStartAnimationEvent evt
) {
1952 if (vbwin
.closed
) return;
1953 if (isQuitRequested
) { vbwin
.close(); return; }
1954 trayStartAnimation();
1957 vbwin
.addEventListener((TrayStopAnimationEvent evt
) {
1958 if (vbwin
.closed
) return;
1959 if (isQuitRequested
) { vbwin
.close(); return; }
1960 trayStopAnimation();
1963 vbwin
.addEventListener((TraySetupAnimationEvent evt
) {
1964 if (vbwin
.closed
) return;
1965 if (isQuitRequested
) { vbwin
.close(); return; }
1966 setupTrayAnimation();
1969 vbwin
.addEventListener((TrayAnimationStepEvent evt
) {
1970 if (vbwin
.closed
) return;
1971 if (isQuitRequested
) { vbwin
.close(); return; }
1972 trayDoAnimationStep();
1975 HintWindow uphintWindow
;
1977 vbwin
.addEventListener((UpdatingAccountEvent evt
) {
1978 if (evt
.accName
.length
) {
1979 if (uphintWindow
!is null) {
1980 uphintWindow
.message
= "updating: "~evt
.accName
;
1982 uphintWindow
= new HintWindow("updating: "~evt
.accName
);
1983 uphintWindow
.winy
= guiThreadListHeight
+1+(3*gxTextHeightUtf
+2-uphintWindow
.winh
)/2;
1985 postScreenRebuild();
1989 vbwin
.addEventListener((UpdatingAccountCompleteEvent evt
) {
1990 if (evt
.accName
.length
&& uphintWindow
!is null) {
1991 uphintWindow
.message
= "done: "~evt
.accName
;
1992 postScreenRebuild();
1996 vbwin
.addEventListener((UpdatingCompleteEvent evt
) {
1998 uphintWindow
.close();
1999 uphintWindow
= null;
2001 foreach (Folder
fld; folders
) if (fld.needRebuild
) fld.buildVisibleList();
2002 setupTrayAnimation(); // check if we have to start/stop animation, and do it
2004 vbwin
.redrawOpenGlSceneNow();
2007 vbwin
.addEventListener((DoCheckBoxesCycleEvent evt
) {
2008 //conwriteln("updating group '", groups[evt.gidx].mbase.groupname);
2009 if (!atomicLoad(updateInProgress
)) {
2010 updateThreadId
.send(UpThreadCommand
.StartUpdate
);
2014 vbwin
.addEventListener((ArticleTextScrollEvent evt
) {
2015 if (vbwin
is null || vbwin
.closed
) return;
2016 if (mainPane
is null) return;
2017 mainPane
.doScrollStep();
2020 vbwin
.redrawOpenGlScene
= delegate () {
2021 if (vbwin
.closed
) return;
2023 bool resizeWin
= false;
2024 bool rebuildTexture
= false;
2028 scope(exit
) consoleUnlock();
2030 if (!conQueueEmpty()) postDoConCommands();
2032 if (VBufScale
!= vbufEffScale
) {
2033 // window scale changed
2034 vbufEffScale
= VBufScale
;
2037 if (vbufEffVSync
!= vbufVSync
) {
2038 vbufEffVSync
= vbufVSync
;
2039 vbwin
.vsync
= vbufEffVSync
;
2044 vbwin
.resize(winWidthScaled
, winHeightScaled
);
2045 glconResize(winWidthScaled
, winHeightScaled
);
2046 rebuildTexture
= true;
2049 if (rebuildTexture
) repaintScreen();
2051 glMatrixMode(GL_PROJECTION
); // for ortho camera
2053 // left, right, bottom, top, near, far
2054 //glViewport(0, 0, w*vbufEffScale, h*vbufEffScale);
2055 //glOrtho(0, w, h, 0, -1, 1); // top-to-bottom
2056 glViewport(0, 0, VBufWidth
*vbufEffScale
, VBufHeight
*vbufEffScale
);
2057 glOrtho(0, VBufWidth
, VBufHeight
, 0, -1, 1); // top-to-bottom
2058 glMatrixMode(GL_MODELVIEW
);
2061 glEnable(GL_TEXTURE_2D
);
2062 glDisable(GL_LIGHTING
);
2063 glDisable(GL_DITHER
);
2064 //glDisable(GL_BLEND);
2065 glDisable(GL_DEPTH_TEST
);
2066 //glEnable(GL_BLEND);
2067 //glBlendFunc(GL_SRC_ALPHA, GL_ONE);
2068 //glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
2069 glDisable(GL_BLEND
);
2070 //glDisable(GL_STENCIL_TEST);
2073 immutable w
= VBufWidth
;
2074 immutable h
= VBufHeight
;
2076 glColor4f(1, 1, 1, 1);
2077 glBindTexture(GL_TEXTURE_2D
, vglTexId
);
2078 //scope(exit) glBindTexture(GL_TEXTURE_2D, 0);
2080 glTexCoord2f(0.0f, 0.0f); glVertex2i(0, 0); // top-left
2081 glTexCoord2f(1.0f, 0.0f); glVertex2i(w
, 0); // top-right
2082 glTexCoord2f(1.0f, 1.0f); glVertex2i(w
, h
); // bottom-right
2083 glTexCoord2f(0.0f, 1.0f); glVertex2i(0, h
); // bottom-left
2087 if (vArrowTextureId
) {
2088 if (isMouseVisible
) {
2089 int px
= lastMouseX
;
2090 int py
= lastMouseY
;
2092 //glBlendFunc(GL_SRC_ALPHA, GL_ONE);
2093 glBlendFunc(GL_SRC_ALPHA
, GL_ONE_MINUS_SRC_ALPHA
);
2094 glColor4f(1, 1, 1, 1);
2095 glBindTexture(GL_TEXTURE_2D
, vArrowTextureId
);
2096 //scope(exit) glBindTexture(GL_TEXTURE_2D, 0);
2098 glTexCoord2f(0.0f, 0.0f); glVertex2i(px
, py
); // top-left
2099 glTexCoord2f(1.0f, 0.0f); glVertex2i(px
+16, py
); // top-right
2100 glTexCoord2f(1.0f, 1.0f); glVertex2i(px
+16, py
+8); // bottom-right
2101 glTexCoord2f(0.0f, 1.0f); glVertex2i(px
, py
+8); // bottom-left
2108 if (isQuitRequested()) vbwin
.postEvent(new QuitEvent());
2111 static if (is(typeof(&vbwin
.closeQuery
))) {
2112 vbwin
.closeQuery
= delegate () { concmd("quit"); postDoConCommands(); };
2115 vbwin
.visibleForTheFirstTime
= delegate () {
2117 vbwin
.setAsCurrentOpenGlContext();
2118 vbufEffVSync
= vbufVSync
;
2119 vbwin
.vsync
= vbufEffVSync
;
2121 // initialize OpenGL texture
2123 enum wrapOpt
= GL_REPEAT
;
2124 enum filterOpt
= GL_NEAREST
; //GL_LINEAR;
2125 enum ttype
= GL_UNSIGNED_BYTE
;
2127 glGenTextures(1, &vglTexId
);
2128 if (vglTexId
== 0) assert(0, "can't create OpenGL texture");
2130 //GLint gltextbinding;
2131 //glGetIntegerv(GL_TEXTURE_BINDING_2D, &gltextbinding);
2132 //scope(exit) glBindTexture(GL_TEXTURE_2D, gltextbinding);
2134 glBindTexture(GL_TEXTURE_2D
, vglTexId
);
2135 glTexParameterf(GL_TEXTURE_2D
, GL_TEXTURE_WRAP_S
, wrapOpt
);
2136 glTexParameterf(GL_TEXTURE_2D
, GL_TEXTURE_WRAP_T
, wrapOpt
);
2137 glTexParameteri(GL_TEXTURE_2D
, GL_TEXTURE_MIN_FILTER
, filterOpt
);
2138 glTexParameteri(GL_TEXTURE_2D
, GL_TEXTURE_MAG_FILTER
, filterOpt
);
2139 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
2140 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
2142 GLfloat
[4] bclr
= 0.0;
2143 glTexParameterfv(GL_TEXTURE_2D
, GL_TEXTURE_BORDER_COLOR
, bclr
.ptr
);
2145 glTexImage2D(GL_TEXTURE_2D
, 0, GL_RGBA
, VBufWidth
, VBufHeight
, 0, GLTexType
, GL_UNSIGNED_BYTE
, vglTexBuf
);
2148 createArrowTexture();
2150 glconInit(winWidthScaled
, winHeightScaled
);
2153 vbwin
.redrawOpenGlSceneNow();
2155 updateThreadId
= spawn(&updateThread
, thisTid
);
2157 // create notification icon
2158 if (trayicon
is null) {
2159 auto drv
= vfsAddPak(wrapMemoryRO(iconsZipData
[]), "", "databinz/icons.zip:");
2160 scope(exit
) vfsRemovePak(drv
);
2162 foreach (immutable idx
; 0..6) {
2163 string fname
= "databinz/icons.zip:icons";
2165 fname
~= "/main.png";
2167 import std
.format
: format
;
2168 fname
= "%s/bat%s.png".format(fname
, idx
-1);
2170 auto fl
= VFile(fname
);
2171 if (fl
.size
== 0 || fl
.size
> 1024*1024) throw new Exception("fucked icon");
2172 auto pngraw
= new ubyte[](cast(uint)fl
.size
);
2173 fl
.rawReadExact(pngraw
);
2174 auto img
= readPng(pngraw
);
2175 if (img
is null) throw new Exception("fucked icon");
2176 icons
[idx
] = imageFromPng(img
);
2178 foreach (immutable idx
, MemoryImage img
; icons
[]) {
2179 trayimages
[idx
] = Image
.fromMemoryImage(img
);
2181 vbwin
.icon
= icons
[0];
2182 trayicon
= new NotificationAreaIcon("Chiroptera", trayimages
[0], (MouseButton button
) {
2183 scope(exit
) if (!conQueueEmpty()) postDoConCommands();
2184 if (button
== MouseButton
.left
) vbwin
.switchToWindow();
2185 if (button
== MouseButton
.middle
) concmd("quit");
2187 setupTrayAnimation();
2188 flushGui(); // or it may not redraw itself
2189 } catch (Exception e
) {
2190 conwriteln("ERROR loading icons: ", e
.msg
);
2195 mainPane
= new MainPaneWindow();
2197 postScreenRebuild();
2200 vbwin
.eventLoop(1000*10,
2202 scope(exit
) if (!conQueueEmpty()) postDoConCommands();
2203 if (vbwin
.closed
) return;
2206 scope(exit
) consoleUnlock();
2209 if (isQuitRequested
) { vbwin
.close(); return; }
2211 delegate (KeyEvent event
) {
2212 scope(exit
) if (!conQueueEmpty()) postDoConCommands();
2213 if (vbwin
.closed
) return;
2214 if (isQuitRequested
) { vbwin
.close(); return; }
2215 if (glconKeyEvent(event
)) {
2216 postScreenRepaint();
2219 if ((event
.modifierState
&ModifierState
.numLock
) == 0) {
2220 switch (event
.key
) {
2221 case Key
.Pad0
: event
.key
= Key
.Insert
; break;
2222 case Key
.Pad1
: event
.key
= Key
.End
; break;
2223 case Key
.Pad2
: event
.key
= Key
.Down
; break;
2224 case Key
.Pad3
: event
.key
= Key
.PageDown
; break;
2225 case Key
.Pad4
: event
.key
= Key
.Left
; break;
2226 //case Key.Pad5: event.key = Key.Insert; break;
2227 case Key
.Pad6
: event
.key
= Key
.Right
; break;
2228 case Key
.Pad7
: event
.key
= Key
.Home
; break;
2229 case Key
.Pad8
: event
.key
= Key
.Up
; break;
2230 case Key
.Pad9
: event
.key
= Key
.PageUp
; break;
2231 case Key
.PadEnter
: event
.key
= Key
.Enter
; break;
2232 case Key
.PadDot
: event
.key
= Key
.Delete
; break;
2236 if (event
.key
== Key
.PadEnter
) event
.key
= Key
.Enter
;
2238 if (dispatchEvent(event
)) return;
2239 //postScreenRepaint(); // just in case
2241 delegate (MouseEvent event
) {
2242 scope(exit
) if (!conQueueEmpty()) postDoConCommands();
2243 if (vbwin
.closed
) return;
2244 lastMouseXUnscaled
= event
.x
;
2245 lastMouseYUnscaled
= event
.y
;
2246 if (event
.type
== MouseEventType
.buttonPressed
) lastMouseButton |
= event
.button
;
2247 else if (event
.type
== MouseEventType
.buttonReleased
) lastMouseButton
&= ~event
.button
;
2249 if (dispatchEvent(event
)) return;
2251 delegate (dchar ch
) {
2252 if (vbwin
.closed
) return;
2253 scope(exit
) if (!conQueueEmpty()) postDoConCommands();
2254 if (glconCharEvent(ch
)) {
2255 postScreenRepaint();
2258 if (dispatchEvent(ch
)) return;
2261 saveCurrentFolderAndPosition();
2262 trayimages
[] = null;
2263 if (trayicon
!is null && !trayicon
.closed
) { trayicon
.close(); trayicon
= null; }
2265 updateThreadId
.send(UpThreadCommand
.Quit
);
2266 conProcessQueue(int.max
/4);