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("Up", "article_prev");
182 mainAppBind("Pad8", "article_prev");
183 mainAppBind("Down", "article_next");
184 mainAppBind("Pad2", "article_next");
185 mainAppBind("PageUp", "article_pgup");
186 mainAppBind("Pad9", "article_pgup");
187 mainAppBind("PageDown", "article_pgdown");
188 mainAppBind("Pad3", "article_pgdown");
189 mainAppBind("Home", "article_to_first");
190 mainAppBind("Pad7", "article_to_first");
191 mainAppBind("End", "article_to_last");
192 mainAppBind("Pad1", "article_to_last");
193 mainAppBind("C-Up", "article_scroll_up");
194 mainAppBind("C-Pad8", "article_scroll_up");
195 mainAppBind("C-Down", "article_scroll_down");
196 mainAppBind("C-Pad2", "article_scroll_down");
197 mainAppBind("C-PageUp", "folder_prev");
198 mainAppBind("C-Pad9", "folder_prev");
199 mainAppBind("C-PageDown", "folder_next");
200 mainAppBind("C-Pad3", "folder_next");
201 mainAppBind("C-M-U", "folder_update");
202 mainAppBind("C-H", "article_dump_headers");
203 mainAppBind("C-S-I", "update_all");
204 mainAppBind("C-Backslash", "find_mine");
205 mainAppBind("C-Slash", "article_to_parent");
206 mainAppBind("C-Comma", "article_to_prev_sib");
207 mainAppBind("C-Period", "article_to_next_sib");
208 mainAppBind("C-Insert", "article_copy_url_to_clipboard");
209 mainAppBind("C-M-K", "article_twit_thread");
210 mainAppBind("T", "article_edit_poster_title");
211 mainAppBind("R", "article_reply");
212 mainAppBind("S-P", "new_post");
213 mainAppBind("S-Enter", "article_open_in_browser");
214 mainAppBind("Delete", "article_softdelete_toggle");
218 // ////////////////////////////////////////////////////////////////////////// //
219 enum UpThreadCommand
{
221 StartUpdate
, // start updating now
225 void updateThread (Tid ownerTid
) {
229 MonoTime lastCollect
= MonoTime
.currTime
;
231 receiveTimeout(30.seconds
,
232 (UpThreadCommand cmd
) {
234 case UpThreadCommand
.Ping
: break;
235 case UpThreadCommand
.StartUpdate
: break;
236 case UpThreadCommand
.Quit
: doQuit
= true; break;
241 bool updateProgressSet
= false;
242 foreach (Account acc
; accounts
) {
243 if (acc
.needUpdate
) {
244 updateProgressSet
= true;
245 atomicStore(updateInProgress
, true);
247 if (vbwin
!is null) vbwin
.postEvent(new UpdatingAccountEvent(acc
.name
));
249 } catch (Exception e
) {
250 conwriteln("ERROR UPDATING ACCOUNT '", acc
.name
, "': ", e
.msg
);
252 if (vbwin
!is null) vbwin
.postEvent(new UpdatingAccountCompleteEvent(acc
.name
));
255 if (updateProgressSet
) {
256 if (vbwin
!is null) vbwin
.postEvent(new UpdatingCompleteEvent());
257 atomicStore(updateInProgress
, false);
260 auto ctt
= MonoTime
.currTime
;
261 if ((ctt
-lastCollect
).total
!"minutes" >= 5) {
262 import core
.memory
: GC
;
269 } catch (Throwable e
) {
270 // here, we are dead and fucked (the exact order doesn't matter)
271 import core
.stdc
.stdlib
: abort
;
272 import core
.stdc
.stdio
: fprintf
, stderr
;
273 import core
.memory
: GC
;
274 import core
.thread
: thread_suspendAll
;
275 GC
.disable(); // yeah
276 thread_suspendAll(); // stop right here, you criminal scum!
277 auto s
= e
.toString();
278 fprintf(stderr
, "\n=== FATAL ===\n%.*s\n", cast(uint)s
.length
, s
.ptr
);
279 abort(); // die, you bitch!
284 // ////////////////////////////////////////////////////////////////////////// //
285 void initConsole () {
286 import std
.functional
: toDelegate
;
288 conRegVar
!VBufScale(1, 4, "v_scale", "window scale: [1..3]");
290 conRegVar
!bool("v_vsync", "sync to video refresh rate?",
291 (ConVarBase self
) => vbufVSync
,
292 (ConVarBase self
, bool nv
) {
293 if (vbufVSync
!= nv
) {
300 conRegFunc
!clearBindings("binds_app_clear", "clear main application keybindings");
301 conRegFunc
!setupDefaultBindings("binds_app_default", "*append* default application bindings");
302 conRegFunc
!mainAppBind("bind_app", "add main application binding");
303 conRegFunc
!mainAppUnbind("unbind_app", "remove main application binding");
306 // //////////////////////////////////////////////////////////////////// //
308 import core
.memory
: GC
;
309 conwriteln("starting GC collection...");
312 conwriteln("GC collection complete.");
313 })("gc_collect", "force GC collection cycle");
316 // //////////////////////////////////////////////////////////////////// //
318 auto qww
= new YesNoWindow("Quit?", "Do you really want to quit?", true);
319 qww
.onYes
= () { concmd("quit"); };
321 })("quit_prompt", "quit with prompt");
324 // //////////////////////////////////////////////////////////////////// //
325 conRegFunc
!((ConString url
) {
327 import std
.stdio
: File
;
329 auto pid
= spawnProcess(
330 [getBrowserCommand
, url
.idup
],
331 File("/dev/zero"), File("/dev/null"), File("/dev/null"),
335 })("open_url", "open given url in a browser");
338 // //////////////////////////////////////////////////////////////////// //
340 foreach (Account acc
; accounts
) acc
.forceUpdating();
341 if (!atomicLoad(updateInProgress
)) {
342 updateThreadId
.send(UpThreadCommand
.StartUpdate
);
344 })("update_all", "mark all groups for updating");
347 // //////////////////////////////////////////////////////////////////// //
349 if (folderCur
> 0) folderCur
= folderCur
-1;
351 })("folder_prev", "go to previous group");
354 if (folders
.length
-folderCur
> 1) folderCur
= folderCur
+1;
356 })("folder_next", "go to next group");
359 // //////////////////////////////////////////////////////////////////// //
361 if (auto fld = getActiveFolder
) {
363 setupTrayAnimation();
366 })("mark_unread", "mark current message as unread");
369 if (auto fld = getActiveFolder
) {
371 setupTrayAnimation();
374 })("mark_read", "mark current message as read");
376 conRegFunc
!((bool allowNextGroup
=false) {
377 if (auto fld = getActiveFolder
) {
378 if (!fld.moveToNextUnread(true)) {
379 if (!allowNextGroup
) return;
381 uint fidx
= cast(uint)((folderCur
+1)%folders
.length
);
382 foreach (immutable _
; 0..folders
.length
) {
383 if (folders
[fidx
].moveToNextUnread(true)) {
385 setupTrayAnimation();
389 fidx
= cast(uint)((fidx
+1)%folders
.length
);
393 setupTrayAnimation();
396 })("next_unread", "move to next unread message; bool arg: can skip to next group?");
399 // //////////////////////////////////////////////////////////////////// //
402 if (mainPane !is null && mainPane.articleTextTopLine > 0) {
403 mainPane.articleTextTopLine -= (VBufHeight-guiThreadListHeight-3*10-4)/gxTextHeightUtf;
404 if (mainPane.articleTextTopLine < 0) mainPane.articleTextTopLine = 0;
408 if (mainPane
!is null) mainPane
.scrollByPageUp();
409 })("artext_page_up", "do pageup on article text");
413 if (mainPane !is null) {
414 mainPane.articleTextTopLine += (VBufHeight-guiThreadListHeight-3*10-4)/gxTextHeightUtf;
418 if (mainPane
!is null) mainPane
.scrollByPageDown();
419 })("artext_page_down", "do pagedown on article text");
422 // //////////////////////////////////////////////////////////////////// //
424 if (auto fldx
= getActiveFolder
) {
425 fldx
.withBaseReader((abase
, cur
, top
, alist
) {
426 if (cur
< alist
.length
) {
427 abase
.loadContent(alist
[cur
]);
428 if (auto art
= abase
[alist
[cur
]]) {
429 scope(exit
) art
.releaseContent();
430 auto path
= art
.getHeaderValue("path:");
431 //conwriteln("path: [", path, "]");
432 if (path
.startsWithCI("digitalmars.com!.POSTED.")) {
433 import std
.stdio
: File
;
435 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
436 string id
= art
.msgid
;
437 if (id
.length
> 2 && id
[0] == '<' && id
[$-1] == '>') id
= id
[1..$-1];
438 auto pid
= spawnProcess(
439 [getBrowserCommand
, "http://forum.dlang.org/post/"~id
],
440 File("/dev/zero"), File("/dev/null"), File("/dev/null"),
448 })("article_open_in_browser", "open the current article in browser");
451 if (auto fldx
= getActiveFolder
) {
452 fldx
.withBaseReader((abase
, cur
, top
, alist
) {
453 if (cur
< alist
.length
) {
454 if (auto art
= abase
[alist
[cur
]]) {
455 auto path
= art
.getHeaderValue("path:");
456 if (path
.startsWithCI("digitalmars.com!.POSTED.")) {
457 //http://forum.dlang.org/post/hhyqqpoatbpwkprbvfpj@forum.dlang.org
458 string id
= art
.msgid
;
459 if (id
.length
> 2 && id
[0] == '<' && id
[$-1] == '>') id
= id
[1..$-1];
460 id
= "http://forum.dlang.org/post/"~id
;
461 setClipboardText(vbwin
, id
);
462 setPrimarySelection(vbwin
, id
);
468 })("article_copy_url_to_clipboard", "copy the url of the current article to clipboard");
471 // //////////////////////////////////////////////////////////////////// //
473 if (auto fld = getActiveFolder
) {
477 })("article_prev", "go to previous article");
480 if (auto fld = getActiveFolder
) {
484 })("article_next", "go to next article");
487 if (auto fld = getActiveFolder
) {
491 })("article_pgup", "artiles list: page up");
494 if (auto fld = getActiveFolder
) {
498 })("article_pgdown", "artiles list: page down");
501 if (auto fld = getActiveFolder
) {
505 })("article_scroll_up", "scroll article list up");
508 if (auto fld = getActiveFolder
) {
512 })("article_scroll_down", "scroll article list up");
515 if (auto fld = getActiveFolder
) {
519 })("article_to_first", "go to first article");
522 if (auto fld = getActiveFolder
) {
526 })("article_to_last", "go to last article");
529 // //////////////////////////////////////////////////////////////////// //
531 if (auto fld = getActiveFolder
) {
532 auto postDg
= delegate (Account acc
) {
533 conwriteln("post with account '", acc
.name
, "' (", acc
.mail
, ")");
534 auto pw
= new PostWindow();
535 pw
.from
.str = acc
.realname
~" <"~acc
.mail
~">";
536 pw
.from
.readonly
= true;
537 if (auto nna
= cast(NntpAccount
)acc
) {
538 pw
.title
= "Reply to NNTP "~nna
.server
~":"~nna
.group
;
539 pw
.to
.str = nna
.group
;
540 pw
.to
.readonly
= true;
541 pw
.activeWidget
= pw
.subj
;
543 pw
.title
= "Reply from '"~acc
.name
~"' ("~acc
.mail
~")";
545 pw
.activeWidget
= pw
.to
;
552 auto acc
= fld.findAccountToPost();
554 auto wacc
= new SelectPopBoxWindow(defaultAcc
);
555 wacc
.onSelected
= postDg
;
561 conwriteln("post: no active folder");
563 })("new_post", "post a new article or message");
566 // //////////////////////////////////////////////////////////////////// //
568 if (auto fld = getActiveFolder
) {
569 auto postDg
= delegate (Account acc
) {
570 conwriteln("reply with account '", acc
.name
, "' (", acc
.mail
, ")");
571 fld.withBaseReader((abase
, cur
, top
, alist
) {
572 if (cur
< alist
.length
) {
573 auto aidx
= alist
.ptr
[cur
];
574 abase
.loadContent(aidx
);
575 if (auto art
= abase
[aidx
]) {
576 assert(art
.contentLoaded
);
577 auto atext
= art
.getTextContent
;
578 auto pw
= new PostWindow();
579 pw
.from
.str = acc
.realname
~" <"~acc
.mail
~">";
580 pw
.from
.readonly
= true;
581 if (auto nna
= cast(NntpAccount
)acc
) {
582 pw
.title
= "Reply to NNTP "~nna
.server
~":"~nna
.group
;
583 pw
.to
.str = nna
.group
;
584 pw
.to
.readonly
= true;
586 pw
.title
= "Reply from '"~acc
.name
~"' ("~acc
.mail
~")";
587 pw
.to
.str = art
.fromname
~" <"~art
.frommail
~">";
589 pw
.subj
.str = "Re: "~art
.subj
;
591 string from
= art
.fromname
;
593 auto vp
= from
.indexOf(" via Digitalmars-");
595 from
= from
[0..vp
].xstrip
;
596 if (from
.length
== 0) from
= "anonymous";
601 pw
.ed
.addText(" wrote:\n");
603 foreach (ConString s
; LineIterator
!false(atext
)) {
604 if (s
.length
== 0 || s
[0] == '>') pw
.ed
.addText(">"); else pw
.ed
.addText("> ");
610 pw
.replyto
= art
.msgid
;
611 pw
.references
= art
.getHeaderValue("References").xstrip
.idup
;
614 pw
.activeWidget
= pw
.ed
;
620 auto acc
= fld.findAccountToPost(fld.curidx
);
622 auto wacc
= new SelectPopBoxWindow(defaultAcc
);
623 wacc
.onSelected
= postDg
;
629 })("article_reply", "reply to the current article");
632 // //////////////////////////////////////////////////////////////////// //
634 if (auto fld = getActiveFolder
) {
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 articleBogoMarkHam(art
);
642 //TODO: move out of spam
647 })("article_mark_ham", "mark current article as ham");
651 if (auto fld = getActiveFolder
) {
653 uint didx
= uint.max
;
654 Folder fldspam
= getSpamFolder
;
655 fld.withBaseReader((abase
, cur
, top
, alist
) {
656 if (cur
< alist
.length
) {
657 auto aidx
= alist
.ptr
[cur
];
658 abase
.loadContent(aidx
);
659 if (auto art
= abase
[aidx
]) {
660 assert(art
.contentLoaded
);
661 conwriteln("marking ", art
.msgid
, " as spam");
662 articleBogoMarkSpam(art
);
663 if (fld !is fldspam
) {
664 conwriteln(" should be moved to spam folder");
665 newart
= art
.clone();
672 if (didx
!= uint.max
) {
673 assert(newart
!is null);
674 conwriteln("removing ", newart
.msgid
, " from ", fld.folderPath
);
675 fld.withBaseWriter((abase
) {
676 if (auto art
= abase
[didx
]) {
677 art
.softDeleted
= true;
679 abase
.writeUpdates();
681 //abase.remove(didx);
682 //abase.writeUpdates();
684 fld.markForRebuild();
685 fld.buildVisibleList();
688 if (newart
!is null && fldspam
!is null) {
689 newart
.unread
= false;
690 conwriteln("adding ", newart
.msgid
, " to ", fldspam
.folderPath
);
691 fldspam
.withBaseWriter((abase
) {
692 abase
.insert(newart
);
693 abase
.writeUpdates();
695 newart
.releaseContent();
696 fldspam
.markForRebuild();
697 fldspam
.buildVisibleList();
701 })("article_mark_spam", "mark current article as spam");
704 // //////////////////////////////////////////////////////////////////// //
705 conRegFunc
!((ConString fldname
) {
706 if (auto fld = getActiveFolder
) {
707 Folder destfld
= findFolderByPath(fldname
);
708 if (destfld
is null) { conwriteln("cannot find folder '", fldname
, "'"); }
709 if (destfld
is fld) { conwriteln("cannot move to the same folder"); return; }
711 uint didx
= uint.max
;
712 fld.withBaseReader((abase
, cur
, top
, alist
) {
713 if (cur
< alist
.length
) {
714 auto aidx
= alist
.ptr
[cur
];
715 abase
.loadContent(aidx
);
716 if (auto art
= abase
[aidx
]) {
717 assert(art
.contentLoaded
);
718 newart
= art
.clone();
723 if (didx
== uint.max || newart
is null) { conwriteln("article not found!"); return; }
725 assert(didx
!= uint.max
);
726 assert(newart
!is null);
727 conwriteln("removing ", newart
.msgid
, " from ", fld.folderPath
);
728 fld.withBaseWriter((abase
) {
729 if (auto art
= abase
[didx
]) {
730 art
.softDeleted
= true;
732 abase
.writeUpdates();
734 //abase.remove(didx);
735 //abase.writeUpdates();
737 fld.markForRebuild();
738 fld.buildVisibleList();
740 conwriteln("adding ", newart
.msgid
, " to ", destfld
.folderPath
);
741 destfld
.withBaseWriter((abase
) {
742 abase
.insert(newart
);
743 abase
.writeUpdates();
745 newart
.releaseContent();
746 destfld
.markForRebuild();
747 destfld
.buildVisibleList();
750 })("article_move_to_folder", "move article to existing folder");
753 // //////////////////////////////////////////////////////////////////// //
755 if (auto fld = getActiveFolder
) {
759 })("article_to_parent", "jump to parent article, if any");
762 if (auto fld = getActiveFolder
) {
766 })("article_to_prev_sib", "jump to previous sibling");
769 if (auto fld = getActiveFolder
) {
773 })("article_to_next_sib", "jump to next sibling");
776 // //////////////////////////////////////////////////////////////////// //
778 if (auto fld = getActiveFolder
) {
779 if (auto acc
= findNntpAccountForFolder(fld)) {
780 fld.withBaseReader(delegate (abase
, cur
, top
, alist
) {
781 if (cur
< alist
.length
) {
782 if (auto art
= abase
[alist
[cur
]]) {
783 auto t
= fld.isTwittedNL(cur
);
784 auto setdg
= delegate (string name
, string mail
, string folder
, string title
) {
787 if (title
.length
!= 0) {
788 concmdfex
!"twit_set name \"%s\" mail \"%s\" folder_mask \"%s\" title \"%s\""((ConString cmd
) { xcmd
= cmd
.xstrip
.idup
; }, name
, mail
, folder
, title
);
791 if (title
.length
== 0) {
792 concmdfex
!"twit_unset name \"%s\" mail \"%s\" folder_mask \"%s\""((ConString cmd
) { xcmd
= cmd
.xstrip
.idup
; }, name
, mail
, folder
);
794 concmdfex
!"twit_set name \"%s\" mail \"%s\" folder_mask \"%s\" title \"%s\""((ConString cmd
) { xcmd
= cmd
.xstrip
.idup
; }, name
, mail
, folder
, title
);
798 import std
.path
: buildPath
;
799 auto fo
= VFile(buildPath(mailRootDir
, "auto_twits.rc"), "a");
801 } catch (Exception e
) {
802 conwriteln("ERROR writing twit info: ", e
.msg
);
806 auto tw
= new TitlerWindow(art
.fromname
, art
.frommail
, (acc
.inboxFolder
.startsWith("dmars_ng/") ?
"dmars_ng/*" : acc
.inboxFolder
), t
);
807 tw
.onSelected
= setdg
;
813 })("article_edit_poster_title", "edit poster's title of the current article");
816 // //////////////////////////////////////////////////////////////////// //
818 if (auto fld = getActiveFolder
) {
819 fld.withBaseReader((abase
, cur
, top
, alist
) {
820 if (cur
< alist
.length
) {
821 uint ridx
= alist
[cur
];
822 if (auto art
= abase
[ridx
]) {
823 while (abase
[ridx
].parent
!= 0) ridx
= abase
[ridx
].parent
;
824 twitThread(fld.folderPath
, art
.msgid
);
825 //TODO: mark thread articles as read
831 })("article_twit_thread", "twit current thread");
834 if (auto fld = getActiveFolder
) {
835 if (fld.toggleSoftDeleted()) postScreenRebuild();
837 })("article_softdelete_toggle", "toggle \"soft deleted\" flag on current article");
840 // //////////////////////////////////////////////////////////////////// //
842 if (auto fld = getActiveFolder
) {
843 if (fld.curidxValid
) {
844 fld.withBaseReader(delegate (abase
, cur
, top
, alist
) {
845 auto art
= abase
[alist
[cur
]];
847 conwriteln("============================");
848 abase
.loadContent(alist
[cur
]);
849 scope(exit
) art
.releaseContent();
851 foreach (ConString s
; art
.headersIterator
!false) {
853 if (from
.length
== 0 && s
.startsWithCI("From:")) from
= s
;
855 conwriteln("---------------");
856 conwriteln(" ", art
.fromname
, " <", art
.frommail
, ">");
857 conwriteln(" ", decodeq(from
));
860 foreach (char ch
; art
.fromname
) {
861 if (dc
.decode(cast(ubyte)ch
)) {
862 if (dc
.codepoint
> 127 || dc
.codepoint
< 32) conwritef
!" \\u%04X "(cast(uint)dc
.codepoint
);
863 else conwrite(cast(char)dc
.codepoint
);
871 })("article_dump_headers", "dump article headers");
874 if (auto fld = getActiveFolder
) {
878 })("folder_rebuild_index", "rebuild index file");
881 if (auto fld = getActiveFolder
) {
885 })("folder_pack_textdb", "pack (rebuild) text database for current folder");
887 conRegVar("folder_hide_old", "should old articles be hidden in UI?",
889 if (auto fld = getActiveFolder
) return fld.hideOldThreads
;
892 delegate (self
, bool nv
) {
893 if (auto fld = getActiveFolder
) fld.hideOldThreads
= nv
;
899 if (auto fld = getActiveFolder) {
900 fld.withBase(delegate (abase) {
901 uint idx = fld.curidx;
902 if (idx >= fld.length) {
904 } else if (auto art = abase[fld.baseidx(idx)]) {
905 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
906 idx = (idx+1)%fld.length;
909 foreach (immutable _; 0..fld.length) {
910 auto art = abase[fld.baseidx(idx)];
912 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
913 fld.curidx = cast(int)idx;
918 idx = (idx+1)%fld.length;
922 })("find_mine", "find mine article");
925 // //////////////////////////////////////////////////////////////////// //
928 })("dbg_font_window", "show window with font");
930 conRegVar
!dbg_dump_keynames("dbg_dump_key_names", "dump key names");
933 // //////////////////////////////////////////////////////////////////// //
934 conRegFunc
!((uint idx
, ConString fname
=null) {
935 import std
.format
: format
;
936 if (auto fld = getActiveFolder
) {
937 fld.withBaseReader(delegate (abase
, cur
, top
, alist
) {
938 if (cur
>= alist
.length
) return;
939 if (auto art
= abase
[alist
[cur
]]) {
940 abase
.loadContent(alist
[cur
]);
941 scope(exit
) art
.releaseContent();
943 art
.writeAttachmentToFile(idx
, delegate (ConString attname
) {
944 //import std.format : format;
945 import std
.file
: isDir
;
946 if (fname
.length
== 0) return "/tmp/"~Article
.fixAttachmentName(attname
);
948 string res
= fname
.idup
;
949 if (res
.length
&& res
[$-1] != '/') res
~= '/';
950 res
~= Article
.fixAttachmentName(attname
);
955 } catch (Exception e
) {
956 conwriteln("ERROR writing attachment");
961 { import core
.memory
: GC
; GC
.collect(); GC
.minimize(); }
962 })("attach_save", "save attach: attach_save index [filename]");
966 // ////////////////////////////////////////////////////////////////////////// //
967 //FIXME: turn into property
968 final class ArticleTextScrollEvent
{}
969 __gshared ArticleTextScrollEvent evArticleScroll
;
970 shared static this () {
971 evArticleScroll
= new ArticleTextScrollEvent();
974 void postArticleScroll () {
975 if (vbwin
!is null && !vbwin
.eventQueued
!ArticleTextScrollEvent
) vbwin
.postTimeout(evArticleScroll
, 25);
979 __gshared MainPaneWindow mainPane
;
982 final class MainPaneWindow
: SubWindow
{
983 string
[] emlines
; // in koi
984 uint emlinesBeforeAttaches
;
985 string lastDecodedMsgFolderPath
;
986 string lastDecodedMsgId
;
987 int articleTextTopLine
= 0;
988 int articleDestTextTopLine
= 0;
991 super(null, 0, 0, VBufWidth
, VBufHeight
);
992 mType
= Type
.OnBottom
;
996 // //////////////////////////////////////////////////////////////////// //
997 static struct WebLink
{
999 int x
, len
; // in pixels
1001 string text
; // visual text
1004 @property bool isAttach () const pure nothrow @safe @nogc { return (attachnum
>= 0); }
1008 int lastUrlIndex
= -1;
1010 void clearDecodedText () {
1011 emurls
[] = WebLink
.init
;
1014 emurls
.assumeSafeAppend
;
1016 emlines
.assumeSafeAppend
;
1017 lastDecodedMsgFolderPath
= null;
1018 lastDecodedMsgId
= null;
1019 articleTextTopLine
= 0;
1020 articleDestTextTopLine
= 0;
1025 int findUrlIndexAt (int mx
, int my
) {
1026 int tpX0
= guiGroupListWidth
+2+1;
1027 int tpX1
= VBufWidth
-1;
1028 int tpY0
= guiThreadListHeight
+1;
1029 int tpY1
= VBufHeight
-1;
1031 int y
= tpY0
+3*gxTextHeightUtf
+2;
1033 if (mx
< tpX0 || mx
> tpX1
) return -1;
1034 if (my
< y || my
> tpY1
) return -1;
1038 // yeah, i can easily calculate this, i know
1039 uint idx
= articleTextTopLine
;
1040 while (idx
< emlines
.length
&& y
< VBufHeight
) {
1041 if (my
>= y
&& my
< y
+gxTextHeightUtf
) {
1042 foreach (immutable uidx
, const ref WebLink wl
; emurls
) {
1044 if (mx
>= wl
.x
&& mx
< wl
.x
+wl
.len
) return cast(int)uidx
;
1049 y
+= gxTextHeightUtf
;
1055 WebLink
* findUrlAt (int mx
, int my
) {
1056 auto uidx
= findUrlIndexAt(mx
, my
);
1057 return (uidx
>= 0 ?
&emurls
[uidx
] : null);
1060 private void emlDetectUrls (uint textlines
) {
1061 static immutable string
[3] protos
= [ "https", "http", "ftp" ];
1065 static sptrdiff
urlepos (const(char)[] s
, sptrdiff spos
) {
1066 assert(spos
< s
.length
);
1067 spos
= s
.indexOf("://", spos
);
1071 while (spos
< s
.length
) {
1073 if (ch
== '/') break;
1074 if (ch
<= ' ') return spos
;
1075 if (ch
!= '.' && ch
!= '-' && !ch
.isalnum
) return spos
;
1078 if (spos
>= s
.length
) return spos
;
1080 assert(s
[spos
] == '/');
1081 while (spos
< s
.length
) {
1083 if (ch
<= ' ' || ch
== '<' || ch
== '>') return spos
;
1084 if (ch
== '.' || ch
== ')' || ch
== '-' || ch
== '!' || ch
== ';') {
1085 if (s
.length
-spos
> 1) {
1087 if (ch
<= ' ' || ch
== '.' || ch
== '(' || ch
== ')' || ch
== '-' || ch
== '!' || ch
== ';' || ch
== '<' || ch
== '>') return spos
;
1095 if (textlines
> emlines
.length
) textlines
= cast(uint)emlines
.length
; // just in case
1096 foreach (immutable cy
, string s
; emlines
[0..textlines
]) {
1097 if (s
.length
== 0) continue;
1098 auto pos
= s
.indexOf("://");
1102 foreach (string proto
; protos
) {
1103 if (spos
>= proto
.length
&& proto
.strEquCI(s
[spos
-proto
.length
..spos
])) {
1104 if (spos
== proto
.length ||
!s
[spos
-proto
.length
-1].isalnum
) {
1106 spos
-= proto
.length
;
1113 auto epos
= urlepos(s
, spos
);
1114 //conwriteln("spos=", spos, "; epos=", epos, "; s=[", s, "]; link=[", s[spos..epos], "]");
1116 wl
.ly
= cast(int)cy
;
1117 wl
.x
= gxTextWidthP(s
[0..spos
]);
1118 wl
.len
= gxTextWidthP(s
[spos
..epos
]);
1119 wl
.url
= wl
.text
= s
[spos
..epos
];
1120 if (spos
> 0) ++wl
.x
;
1126 //conwriteln("pos=", pos, "; s=[", s, "]; rest=[", s[pos..$], "]");
1127 pos
= s
.indexOf("://", pos
);
1131 int attachcount
= 0;
1132 foreach (immutable uint cy
; textlines
..cast(uint)emlines
.length
) {
1133 string s
= emlines
[cy
];
1134 if (s
.length
== 0) continue;
1135 auto spos
= s
.indexOf("attach:");
1136 if (spos
< 0) continue;
1138 while (epos
< s
.length
&& s
.ptr
[epos
] > ' ') ++epos
;
1139 //if (attachcount >= parts.length) break;
1141 wl
.ly
= cast(int)cy
;
1142 wl
.x
= gxTextWidthP(s
[0..spos
]);
1143 wl
.len
= gxTextWidthP(s
[spos
..epos
]);
1144 wl
.url
= wl
.text
= s
[spos
..epos
];
1145 if (spos
> 0) ++wl
.x
;
1146 wl
.attachnum
= attachcount
;
1147 //wl.attachfname = s[spos+7..epos];
1148 //wl.part = parts[attachcount];
1154 bool needToDecodeArticleTextNL (Folder
fld, Article art
) nothrow @trusted @nogc {
1155 if (art
is null) return false;
1156 if (lastDecodedMsgId
!= art
.msgid
) return true;
1157 return (fld !is null ? lastDecodedMsgFolderPath
!= fld.folderPath
: lastDecodedMsgFolderPath
.length
!= 0);
1160 // fld is locked here
1161 void decodeArticleTextNL(bool doLocalConvert
=true) (Folder
fld, Article art
) {
1162 static if (doLocalConvert
) {
1163 if (art
is null) { clearDecodedText(); return; }
1164 if (!needToDecodeArticleTextNL(fld, art
)) return;
1168 emlines
~= null; // hack; this dummy line will be removed
1169 lastDecodedMsgFolderPath
= (fld !is null ?
fld.folderPath
: null);
1170 lastDecodedMsgId
= art
.msgid
;
1172 bool lastEndsWithSpace () { return (emlines
[$-1].length ? emlines
[$-1][$-1] == ' ' : false); }
1176 void putLine (ConString s
) {
1177 enum QuoteStr
= ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ";
1178 static string
addQuotes(T
:const(char)[]) (T s
, int qlevel
) {
1180 static if (is(T
== string
) ||
is (T
== typeof(null))) return s
; else return s
.idup
;
1182 if (qlevel
> QuoteStr
.length
-1) qlevel
= cast(int)QuoteStr
.length
-1;
1183 return QuoteStr
[$-qlevel
-1..$]~s
;
1185 // calculate quote level
1187 if (s
.length
&& s
[0] == '>') {
1188 usize lastqpos
= 0, pos
= 0;
1189 while (pos
< s
.length
) {
1190 if (s
.ptr
[pos
] != '>') {
1191 if (s
.ptr
[pos
] != ' ') break;
1198 if (s
.length
-lastqpos
> 1 && s
.ptr
[lastqpos
+1] == ' ') ++lastqpos
;
1202 //conwriteln("qlevel=", qlevel, "; s=[", s, "]");
1203 // empty line: just insert it
1204 if (s
.length
== 0) {
1205 emlines
~= addQuotes(null, qlevel
).xstripright
;
1208 bool newline
= false;
1209 if (lastQLevel
!= qlevel ||
!lastEndsWithSpace
) {
1213 //while (s.length > 1 && s.ptr[0] <= ' ') s = s[1..$];
1218 while (epos
< s
.length
&& s
.ptr
[epos
] <= ' ') ++epos
;
1220 //assert(s[0] > ' ');
1221 while (epos
< s
.length
&& s
.ptr
[epos
] <= ' ') ++epos
;
1223 while (epos
< s
.length
&& s
.ptr
[epos
] > ' ') ++epos
;
1225 while (epos
< s
.length
&& s
.ptr
[epos
] <= ' ') ++epos
;
1226 if (!newline
&& emlines
[$-1].length
+xlen
<= 80) {
1227 // no wrapping, continue last line
1228 emlines
[$-1] ~= s
[0..epos
];
1231 // wrapping; new line
1232 emlines
~= addQuotes(s
[0..epos
], qlevel
);
1236 if (newline
) emlines
~= addQuotes(null, qlevel
);
1238 lastQLevel
= qlevel
;
1242 foreach (ConString s
; LineIterator
!false(art
.getTextContent
)) {
1243 static if (doLocalConvert
) {
1244 putLine(s
.uniRecode
);
1250 // remove first dummy line
1251 if (emlines
.length
) emlines
= emlines
[1..$];
1252 // remove trailing empty lines
1253 while (emlines
.length
&& emlines
[$-1].xstrip
.length
== 0) emlines
.length
-= 1;
1254 } catch (Exception e
) {
1255 conwriteln("================================= ERROR: ", e
.msg
, " =================================");
1256 conwriteln(e
.toString
);
1260 auto lcount
= cast(uint)emlines
.length
;
1261 emlinesBeforeAttaches
= lcount
;
1263 static if (doLocalConvert
) {
1265 art
.forEachAttachment(delegate(ConString type
, ConString filename
) {
1266 if (attcount
== 0) { emlines
~= null; emlines
~= null; }
1267 import std
.format
: format
;
1268 if (type
.length
== 0) type
= "unknown/unknown";
1269 string s
= " [%02s] attach:%s -- %s".format(attcount
, Article
.fixAttachmentName(filename
), type
);
1274 emlDetectUrls(lcount
);
1276 emlDetectUrls(lcount
);
1280 @property int visibleArticleLines () {
1281 int y
= guiThreadListHeight
+1+3*gxTextHeightUtf
+2;
1282 return (VBufHeight
-y
)/gxTextHeightUtf
;
1285 void normalizeArticleTopLine () {
1286 int lines
= visibleArticleLines
;
1287 if (lines
< 1 || emlines
.length
<= lines
) {
1288 articleTextTopLine
= 0;
1289 articleDestTextTopLine
= 0;
1291 if (articleTextTopLine
< 0) articleTextTopLine
= 0;
1292 if (articleTextTopLine
+lines
> emlines
.length
) {
1293 articleTextTopLine
= cast(int)emlines
.length
-lines
;
1294 if (articleTextTopLine
< 0) articleTextTopLine
= 0;
1299 void doScrollStep () {
1300 auto oldtop
= articleTextTopLine
;
1302 normalizeArticleTopLine();
1303 if (articleDestTextTopLine
< articleTextTopLine
) {
1304 --articleTextTopLine
;
1305 } else if (articleDestTextTopLine
> articleTextTopLine
) {
1306 ++articleTextTopLine
;
1310 normalizeArticleTopLine();
1312 postScreenRebuild();
1313 if (articleTextTopLine
== oldtop
) {
1314 // can't scroll anymore
1315 articleDestTextTopLine
= articleTextTopLine
;
1318 postArticleScroll();
1321 void scrollBy (int delta
) {
1322 articleDestTextTopLine
+= delta
;
1326 void scrollByPageUp () {
1327 int lines
= visibleArticleLines
-1;
1328 if (lines
< 1) lines
= 1;
1332 void scrollByPageDown () {
1333 int lines
= visibleArticleLines
-1;
1334 if (lines
< 1) lines
= 1;
1338 // //////////////////////////////////////////////////////////////////// //
1339 //TODO: move parts to widgets
1340 override void onPaint () {
1343 gxFillRect(0, 0, guiGroupListWidth
, VBufHeight
, gxRGB
!(20, 20, 20));
1344 gxVLine(guiGroupListWidth
, 0, VBufHeight
, gxRGB
!(255, 255, 255));
1346 gxFillRect(guiGroupListWidth
+1, 0, VBufWidth
, guiThreadListHeight
, gxRGB
!(15, 15, 15));
1347 gxHLine(guiGroupListWidth
+1, guiThreadListHeight
, VBufWidth
, gxRGB
!(255, 255, 255));
1349 // called with locked folder
1350 void drawArticle (ArticleBase abase
, Folder
fld, Article art
, uint cidx
, uint aidx
) {
1351 import core
.stdc
.stdio
: snprintf
;
1352 import std
.format
: format
;
1353 import std
.datetime
;
1355 const(char)[] tbufs
;
1357 void xfmt (string s
, const(char)[][] strs
...) {
1359 void puts (const(char)[] s
...) {
1360 foreach (char ch
; s
) {
1361 if (dpos
>= tbuf
.length
) break;
1366 if (strs
.length
&& s
.length
> 1 && s
[0] == '%' && s
[1] == 's') {
1375 tbufs
= tbuf
[0..dpos
];
1378 if (needToDecodeArticleTextNL(fld, art
)) {
1379 abase
.loadContent(aidx
);
1380 scope(exit
) abase
.releaseContent(aidx
);
1381 assert(art
.contentLoaded
);
1382 decodeArticleTextNL(fld, art
);
1385 clipX0
= guiGroupListWidth
+2;
1386 clipX1
= VBufWidth
-1;
1387 clipY0
= guiThreadListHeight
+1;
1388 clipY1
= VBufHeight
-1;
1390 int msx
= lastMouseX
;
1391 int msy
= lastMouseY
;
1394 gxFillRect(clipX0
, clipY0
, clipX1
-clipX0
+1, 3*gxTextHeightUtf
+2, gxRGB
!(30, 30, 30));
1395 //gxDrawTextUtf(clipX0+1, clipY0+0*gxTextHeightUtf+1, "From: %s <%s>".format(art.fromname, art.frommail), gxRGB!(0, 128, 128));
1396 //gxDrawTextUtf(clipX0+1, clipY0+1*gxTextHeightUtf+1, "Subject: %s".format(art.subj), gxRGB!(0, 128, 128));
1397 xfmt("From: %s <%s>", art
.fromname
, art
.frommail
);
1398 gxDrawTextUtf(clipX0
+1, clipY0
+0*gxTextHeightUtf
+1, tbufs
, gxRGB
!(0, 128, 128));
1399 xfmt("Subject: %s", art
.subj
);
1400 gxDrawTextUtf(clipX0
+1, clipY0
+1*gxTextHeightUtf
+1, tbufs
, gxRGB
!(0, 128, 128));
1402 auto t
= SysTime
.fromUnixTime(art
.time
);
1403 //string s = "Date: %04d/%02d/%02d %02d:%02d:%02d".format(t.year, t.month, t.day, t.hour, t.minute, t.second);
1404 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
);
1405 gxDrawTextUtf(clipX0
+1, clipY0
+2*gxTextHeightUtf
+1, tbuf
[0..tlen
], gxRGB
!(0, 128, 128));
1409 int y
= clipY0
+3*gxTextHeightUtf
+2;
1412 normalizeArticleTopLine();
1414 bool drawUpMark
= (articleTextTopLine
> 0);
1415 bool drawDownMark
= false;
1417 uint idx
= articleTextTopLine
;
1418 bool msvisible
= isMouseVisible
;
1419 while (idx
< emlines
.length
&& y
< VBufHeight
) {
1421 string s
= emlines
[idx
];
1422 uint clr
= gxRGB
!(0, 128, 0);
1424 foreach (char ch
; s
) {
1425 if (ch
<= ' ') continue;
1426 if (ch
!= '>') break;
1430 clr
= gxRGB
!( 0, 128+40, 0);
1432 final switch (qlevel
%2) {
1433 case 0: clr
= gxRGB
!(128, 128, 0); break;
1434 case 1: clr
= gxRGB
!( 0, 128, 128); break;
1438 gxDrawTextP(clipX0
+1, y
, s
, clr
);
1440 foreach (const ref WebLink wl
; emurls
) {
1442 uint lclr
= gxRGB
!(0, 200, 200);
1443 if (msvisible
&& msy
>= y
&& msy
< y
+gxTextHeightUtf
&& msx
>= clipX0
+1+wl
.x
&& msx
< clipX0
+1+wl
.x
+wl
.len
) {
1444 lclr
= (lastMouseLeft ? gxRGB
!(255, 0, 255) : gxRGB
!(0, 255, 255));
1446 gxDrawTextP(clipX0
+1+wl
.x
, y
, wl
.text
, lclr
);
1450 if (clipY1
-y
< gxTextHeightUtf
&& emlines
.length
-idx
> 0) drawDownMark
= true;
1453 y
+= gxTextHeightUtf
;
1456 if (drawUpMark
) gxDrawTextOutP(clipX1
-gxTextWidthP(triangleDownStr
)-3, sty
, triangleDownStr
, gxRGB
!(255, 255, 255), gxRGB
!(0, 69, 69));
1457 if (drawDownMark
) gxDrawTextOutP(clipX1
-gxTextWidthP(triangleUpStr
)-3, clipY1
-7, triangleUpStr
, gxRGB
!(255, 255, 255), gxRGB
!(0, 69, 69));
1459 string twittext
= fld.isTwittedNL(cidx
);
1460 if (twittext
!is null) {
1461 foreach (immutable dy
; clipY0
+3*gxTextHeightUtf
+2..clipY1
+1) {
1462 foreach (immutable dx
; clipX0
..clipX1
+1) {
1463 if ((dx^dy
)&1) gxPutPixel(dx
, dy
, gxRGBA
!(0, 0, 80, 127));
1467 if (twittext
.length
) {
1468 int tx
= clipX0
+(clipWidth
-gxTextWidthScaledUtf(3, twittext
))/2-1;
1469 int ty
= clipY0
+(clipHeight
-3*gxTextHeightUtf
)/2-1;
1470 foreach (immutable dy
; -1..2) {
1471 foreach (immutable dx
; -1..2) {
1472 if (dx || dy
) gxDrawTextScaledUtf(3, tx
+dx
, ty
+dy
, twittext
, 0);
1475 gxDrawTextScaledUtf(3, tx
, ty
, twittext
, gxRGB
!(255, 0, 0));
1480 void drawThreadList (Folder
fld) {
1481 if (fld.needRebuild
) fld.buildVisibleList();
1482 fld.makeCurrentVisible();
1483 fld.withBaseReader(delegate (abase
, cur
, top
, alist
) {
1484 if (alist
.length
== 0) return;
1486 clipX0
= guiGroupListWidth
+2;
1487 clipX1
= VBufWidth
-1-4;
1489 clipY1
= guiThreadListHeight
-1;
1490 immutable uint origX0
= clipX0
;
1491 immutable uint origX1
= clipX1
;
1492 immutable uint origY0
= clipY0
;
1493 immutable uint origY1
= clipY1
;
1495 //conwriteln(fld.msgtop, " : ", fld.list.length);
1497 while (idx
< alist
.length
&& y
< guiThreadListHeight
) {
1498 import std
.format
: format
;
1499 import std
.datetime
;
1501 if (y
>= guiThreadListHeight
) break;
1502 if (idx
>= alist
.length
) break;
1507 auto art
= abase
[alist
[idx
]];
1509 //conwriteln(idx, " : ", fld.list.length);
1511 uint cc
= gxRGB
!(0, 127, 127);
1512 if (art
.softDeleted
) cc
= gxRGB
!(0, 127-30, 127-30);
1513 gxFillRect(clipX0
, y
, clipWidth
-1, gxTextHeightUtf
, cc
);
1518 //uint clr = (idx != fld.curidx ? gxRGB!(255, 127, 0) : gxRGB!(255, 255, 255));
1519 uint clr
= (art
.unread ? gxRGB
!(255, 255, 255) : gxRGB
!(255-60, 127-60, 0));
1520 uint clr1
= (art
.unread ? gxRGB
!(255, 255, 0) : gxRGB
!(255-60-40, 127-60-40, 0));
1522 if (art
.depth
!= 0 && !art
.unread
) {
1523 clr
= gxRGB
!(255-90, 127-90, 0);
1524 clr1
= gxRGB
!(255-90-40, 127-90-40, 0);
1527 if (!art
.unread
&& !art
.softDeleted
) {
1528 if (fld.isTwittedNL(idx
) !is null) { clr
= gxRGB
!(60, 0, 0); clr1
= gxRGB
!(60, 0, 0); }
1529 if (fld.isHighlightedNL(idx
)) { clr
= gxRGB
!(0, 190, 0); clr1
= gxRGB
!(0, 190-40, 0); }
1530 } else if (art
.softDeleted
) {
1531 //clr = gxRGB!(255-80, 127-80, 0);
1532 //clr1 = gxRGB!(255-80-40, 127-80-40, 0);
1533 clr
= clr1
= gxRGB
!(127, 0, 0);
1536 auto t
= SysTime
.fromUnixTime(art
.time
);
1537 //string s = "%04d/%02d/%02d %02d:%02d".format(t.year, t.month, t.day, t.hour, t.minute);
1539 import core
.stdc
.stdio
: snprintf
;
1541 //string s = "%02d/%02d %02d:%02d".format(t.month, t.day, t.hour, t.minute);
1542 auto len
= snprintf(tmpbuf
.ptr
, tmpbuf
.length
, "%02d/%02d/%02d %02d:%02d", t
.year
%100, t
.month
, t
.day
, t
.hour
, t
.minute
);
1543 gxDrawTextUtf(clipX1
-gxTextWidthUtf(tmpbuf
[0..len
]), y
, tmpbuf
[0..len
], clr
);
1546 string from
= art
.fromname
;
1548 auto vp
= from
.indexOf(" via Digitalmars-");
1550 from
= from
[0..vp
].xstrip
;
1551 if (from
.length
== 0) from
= "anonymous";
1556 gxDrawTextUtf(clipX1
-22*6, y
, from
, clr
);
1557 gxDrawTextUtf(clipX1
-22*6+gxTextWidthUtf(from
)+4, y
, "<", clr1
);
1558 gxDrawTextUtf(clipX1
-22*6+gxTextWidthUtf(from
)+4+gxTextWidthUtf("<")+1, y
, art
.frommail
, clr1
);
1559 gxDrawTextUtf(clipX1
-22*6+gxTextWidthUtf(from
)+4+gxTextWidthUtf("<")+1+gxTextWidthUtf(art
.frommail
)+1, y
, ">", clr1
);
1562 gxDrawTextUtf(clipX0
+art
.depth
*3, y
, art
.subj
, clr
);
1563 foreach (immutable dx
; 0..art
.depth
) gxPutPixel(clipX0
+1+dx
*3, y
+gxTextHeightUtf
/2, gxRGB
!(70, 70, 70));
1565 if (art
.softDeleted
) {
1568 gxHLine(clipX0
, y
+gxTextHeightUtf
/2, clipX1
-clipX0
+1, clr
);
1572 y
+= gxTextHeightUtf
;
1577 //if (idx > fld.list.length) idx = cast(uint)fld.list.length;
1582 int hgt
= clipY1
-clipY0
+1-4;
1583 int pix
= cast(int)(cast(long)hgt
*idx
/alist
.length
);
1584 if (pix
> hgt
) pix
= hgt
;
1585 gxVLine(clipX1
-2, clipY0
+2, pix
, gxRGB
!(160, 160, 160));
1587 gxVLine(clipX1
-3, clipY0
+2, hgt
, gxRGB
!(220, 220, 220));
1588 gxVLine(clipX1
-1, clipY0
+2, hgt
, gxRGB
!(220, 220, 220));
1589 gxHLine(clipX1
-2, clipY0
+1, 1, gxRGB
!(220, 220, 220));
1590 gxHLine(clipX1
-2, clipY1
-1, 1, gxRGB
!(220, 220, 220));
1593 if (cur
< alist
.length
) drawArticle(abase
, fld, abase
[alist
[cur
]], cur
, alist
[cur
]);
1598 folderMakeCurVisible();
1601 foreach (immutable idx
, Folder
fld; folders
) {
1602 if (idx
< folderTop
) continue;
1603 if (ofsy
>= VBufHeight
) break;
1605 if (idx
== folderCur
) gxFillRect(0, ofsy
-1, guiGroupListWidth
, (gxTextHeightUtf
+2), gxRGB
!(0, 127, 127));
1608 clipX1
= guiGroupListWidth
-3;
1609 int depth
= folderDepth(idx
);
1610 uint clr
= gxRGB
!(255-30, 127-30, 0);
1611 if (fld.unreadCount
) {
1612 clr
= gxRGB
!(0, 255, 255);
1615 clr
= (fld.folderPath
== "accounts" ? gxRGB
!(220, 220, 0) : gxRGB
!(255, 127+60, 0));
1616 } else if (fld.folderPath
.startsWith("accounts/") && fld.folderPath
.endsWith("/inbox")) {
1617 clr
= gxRGB
!(255, 127+30, 0);
1620 foreach (immutable dd; 0..depth
) gxPutPixel(ofsx
+dd*6+2, ofsy
+gxTextHeightUtf
/2, gxRGB
!(80, 80, 80));
1621 gxDrawTextOutP(ofsx
+depth
*6, ofsy
, folderVisName(idx
), clr
, gxRGB
!(0, 0, 0));
1622 ofsy
+= gxTextHeightUtf
+2;
1626 if (folderCur
< folders
.length
) drawThreadList(folders
[folderCur
]);
1629 override bool onKey (KeyEvent event
) {
1630 if (event
.pressed
) {
1631 if (dbg_dump_keynames
) conwriteln("key: ", event
.toStr
, ": ", event
.modifierState
&ModifierState
.windows
);
1632 //foreach (const ref kv; mainAppKeyBindings.byKeyValue) if (event == kv.key) concmd(kv.value);
1634 if (auto cmdp
= event
.toStrBuf(kname
[]) in mainAppKeyBindings
) {
1640 if (event == "S-Up") {
1641 if (folderTop > 0) --folderTop;
1642 postScreenRebuild();
1645 if (event == "S-Down") {
1646 if (folderTop+1 < folders.length) ++folderTop;
1647 postScreenRebuild();
1651 //if (event == "Tab") { new PostWindow(); return true; }
1652 //if (event == "Tab") { new SelectPopBoxWindow(null); return true; }
1657 // returning `false` to avoid screen rebuilding by dispatcher
1658 override bool onMouse (MouseEvent event
) {
1659 //FIXME: use window coordinates
1661 event
.mouse2xy(mx
, my
);
1663 if (event
.type
== MouseEventType
.buttonPressed
&& event
.button
== MouseButton
.left
) {
1665 if (mx
>= 0 && mx
< guiGroupListWidth
&& my
>= 0 && my
< VBufHeight
) {
1666 uint fnum
= my
/(gxTextHeightUtf
+2)+folderTop
;
1667 if (fnum
!= folderCur
) {
1669 postScreenRebuild();
1674 if (mx
> guiGroupListWidth
&& mx
< VBufWidth
&& my
>= 0 && my
< guiThreadListHeight
) {
1675 if (auto fld = getActiveFolder
) {
1676 my
/= gxTextHeightUtf
;
1677 fld.curidx
= fld.msgtop
+my
;
1678 postScreenRebuild();
1684 if (event
.type
== MouseEventType
.buttonPressed
&& (event
.button
== MouseButton
.wheelUp || event
.button
== MouseButton
.wheelDown
)) {
1686 if (mx
>= 0 && mx
< guiGroupListWidth
&& my
>= 0 && my
< VBufHeight
) {
1687 if (event
.button
== MouseButton
.wheelUp
) {
1688 if (folderCur
> 0) folderCur
= folderCur
-1;
1690 if (folderCur
+1 < folders
.length
) folderCur
= folderCur
+1;
1692 postScreenRebuild();
1696 if (mx
> guiGroupListWidth
&& mx
< VBufWidth
&& my
>= 0 && my
< guiThreadListHeight
) {
1697 if (auto fld = getActiveFolder
) {
1698 if (event
.button
== MouseButton
.wheelUp
) fld.moveUp(); else fld.moveDown();
1699 postScreenRebuild();
1704 if (mx
> guiGroupListWidth
&& mx
< VBufWidth
&& my
> guiThreadListHeight
&& my
< VBufHeight
) {
1705 enum ScrollLines
= 2;
1706 if (event
.button
== MouseButton
.wheelUp
) scrollBy(-ScrollLines
); else scrollBy(ScrollLines
);
1707 postScreenRebuild();
1712 if (event
.type
== MouseEventType
.buttonReleased
&& event
.button
== MouseButton
.left
) {
1714 auto url
= findUrlAt(mx
, my
);
1717 concmdf
!"attach_save %s"(url
.attachnum
);
1719 concmdf
!"open_url \"%s\""(url
.url
);
1723 if (event
.type
== MouseEventType
.motion
) {
1724 auto uidx
= findUrlIndexAt(mx
, my
);
1725 if (uidx
!= lastUrlIndex
) { lastUrlIndex
= uidx
; postScreenRebuild(); return false; }
1728 if (event
.type
== MouseEventType
.buttonPressed || event
.type
== MouseEventType
.buttonReleased
) {
1729 postScreenRebuild();
1731 postScreenRepaint();
1737 override bool onChar (dchar ch
) {
1743 // ////////////////////////////////////////////////////////////////////////// //
1744 void repaintScreen () {
1752 // ////////////////////////////////////////////////////////////////////////// //
1753 class FontWindow
: SubWindow
{
1757 super("", 16*14+4, 16*14+4+10);
1758 //if (hasWindowClass(this)) return;
1762 override void onPaint () {
1763 import core
.stdc
.stdio
: snprintf
;
1766 auto tlen
= snprintf(buf
.ptr
, buf
.length
, "Font: \\x%02x (%d)", cast(int)(cy
*16+cx
), cast(int)(cy
*16+cx
));
1769 gxDrawWindow(buf
[0..tlen
], gxRGB
!(255, 255, 255), gxRGB
!(0, 0, 0), gxRGB
!(255, 255, 255), gxRGB
!(0, 0, 180));
1774 foreach (immutable int dy
; 0..16) {
1775 foreach (immutable int dx
; 0..16) {
1776 if (cx
== dx
&& cy
== dy
) gxFillRect(clipX0
+dx
*14, clipY0
+dy
*14, 12, 13, 0);
1777 gxDrawCharP(clipX0
+dx
*14+2, clipY0
+dy
*14+2, cast(char)(dy
*16+dx
),
1778 ((dx^dy
)&1 ? gxRGB
!(255, 255, 255) : gxRGB
!(255, 255, 0)));
1783 override bool onKey (KeyEvent event
) {
1784 if (event
.pressed
) {
1785 if (event
== "Escape" || event
== "C-Q") { close(); return true; }
1786 if (event
== "Left" || event
== "Pad4") { cx
= (cx
+15)%16; return true; }
1787 if (event
== "Right" || event
== "Pad6") { cx
= (cx
+1)%16; return true; }
1788 if (event
== "Up" || event
== "Pad8") { cy
= (cy
+15)%16; return true; }
1789 if (event
== "Down" || event
== "Pad2") { cy
= (cy
+1)%16; return true; }
1790 if (event
== "Home" || event
== "Pad7") { cx
= 0; return true; }
1791 if (event
== "End" || event
== "Pad1") { cx
= 15; return true; }
1792 if (event
== "PageUp" || event
== "Pad9") { cy
= 0; return true; }
1793 if (event
== "PageDown" || event
== "Pad3") { cy
= 15; return true; }
1795 return super.onKey(event
);
1800 // ////////////////////////////////////////////////////////////////////////// //
1801 __gshared LockFile mainLockFile
;
1804 void checkMainLockFile () {
1805 import std
.path
: buildPath
;
1806 mainLockFile
= LockFile(buildPath(mailRootDir
, ".chiroptera.lock"));
1807 if (!mainLockFile
.tryLock
) { mainLockFile
.close(); assert(0, "already running"); }
1811 void main (string
[] args
) {
1812 checkMainLockFile();
1813 scope(exit
) mainLockFile
.close();
1815 sdpyWindowClass
= "Chiroptera";
1816 glconShowKey
= "M-Grave";
1819 hitwitInitConsole();
1822 setupDefaultBindings();
1824 concmd("exec chiroptera.rc tan");
1828 concmdf
!"exec %s/accounts.rc tan"(mailRootDir
);
1829 concmdf
!"exec %s/addressbook.rc tan"(mailRootDir
);
1830 concmdf
!"exec %s/filters.rc tan"(mailRootDir
);
1831 concmdf
!"exec %s/highlights.rc tan"(mailRootDir
);
1832 concmdf
!"exec %s/twits.rc tan"(mailRootDir
);
1833 concmdf
!"exec %s/twit_threads.rc tan"(mailRootDir
);
1834 concmdf
!"exec %s/auto_twits.rc tan"(mailRootDir
);
1835 concmdf
!"exec %s/auto_twit_threads.rc tan"(mailRootDir
);
1836 conProcessQueue(); // load config
1837 conProcessArgs
!true(args
);
1839 vbufEffScale
= VBufScale
;
1840 vbufEffVSync
= vbufVSync
;
1842 lastWinWidth
= winWidthScaled
;
1843 lastWinHeight
= winHeightScaled
;
1845 restoreCurrentFolderAndPosition();
1847 vbwin
= new SimpleWindow(lastWinWidth
, lastWinHeight
, "Chiroptera", OpenGlOptions
.yes
, Resizablity
.allowResizing
);
1850 vbwin
.onFocusChange
= delegate (bool focused
) {
1851 vbfocused
= focused
;
1853 lastMouseButton
= 0;
1854 eguiLostGlobalFocus();
1858 vbwin
.windowResized
= delegate (int wdt
, int hgt
) {
1859 // TODO: fix gui sizes
1860 if (vbwin
.closed
) return;
1862 if (lastWinWidth
== wdt
&& lastWinHeight
== hgt
) return;
1863 glconResize(wdt
, hgt
);
1865 double glwFrac
= cast(double)guiGroupListWidth
/VBufWidth
;
1866 double tlhFrac
= cast(double)guiThreadListHeight
/VBufHeight
;
1868 vbufEffScale
= VBufScale
;
1869 if (wdt
< VBufScale
*32) wdt
= VBufScale
;
1870 if (hgt
< VBufScale
*32) hgt
= VBufScale
;
1871 VBufWidth
= (wdt
+VBufScale
-1)/VBufScale
;
1872 VBufHeight
= (hgt
+VBufScale
-1)/VBufScale
;
1874 import core
.stdc
.stdlib
: realloc
;
1875 auto nv
= cast(uint*)realloc(vglTexBuf
, (VBufWidth
*VBufHeight
+4)*4);
1876 if (nv
is null) assert(0, "out of memory!");
1880 guiGroupListWidth
= cast(int)(glwFrac
*VBufWidth
+0.5);
1881 guiThreadListHeight
= cast(int)(tlhFrac
*VBufHeight
+0.5);
1883 if (guiGroupListWidth
< 12) guiGroupListWidth
= 12;
1884 if (guiThreadListHeight
< 16) guiThreadListHeight
= 16;
1887 lastWinHeight
= hgt
;
1889 // reinitialize OpenGL texture
1893 enum wrapOpt
= GL_REPEAT
;
1894 enum filterOpt
= GL_NEAREST
; //GL_LINEAR;
1895 enum ttype
= GL_UNSIGNED_BYTE
;
1897 if (vglTexId
) glDeleteTextures(1, &vglTexId
);
1899 glGenTextures(1, &vglTexId
);
1900 if (vglTexId
== 0) assert(0, "can't create OpenGL texture");
1902 //GLint gltextbinding;
1903 //glGetIntegerv(GL_TEXTURE_BINDING_2D, &gltextbinding);
1904 //scope(exit) glBindTexture(GL_TEXTURE_2D, gltextbinding);
1906 glBindTexture(GL_TEXTURE_2D
, vglTexId
);
1907 glTexParameterf(GL_TEXTURE_2D
, GL_TEXTURE_WRAP_S
, wrapOpt
);
1908 glTexParameterf(GL_TEXTURE_2D
, GL_TEXTURE_WRAP_T
, wrapOpt
);
1909 glTexParameteri(GL_TEXTURE_2D
, GL_TEXTURE_MIN_FILTER
, filterOpt
);
1910 glTexParameteri(GL_TEXTURE_2D
, GL_TEXTURE_MAG_FILTER
, filterOpt
);
1911 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
1912 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
1914 GLfloat
[4] bclr
= 0.0;
1915 glTexParameterfv(GL_TEXTURE_2D
, GL_TEXTURE_BORDER_COLOR
, bclr
.ptr
);
1917 glTexImage2D(GL_TEXTURE_2D
, 0, GL_RGBA
, VBufWidth
, VBufHeight
, 0, GLTexType
, GL_UNSIGNED_BYTE
, vglTexBuf
);
1923 vbwin
.redrawOpenGlSceneNow();
1926 vbwin
.addEventListener((DoConsoleCommandsEvent evt
) {
1927 bool sendAnother
= false;
1928 bool prevVisible
= isConsoleVisible
;
1931 scope(exit
) consoleUnlock();
1933 sendAnother
= !conQueueEmpty();
1935 if (sendAnother
) postDoConCommands();
1936 if (vbwin
.closed
) return;
1937 if (isQuitRequested
) { vbwin
.close(); return; }
1938 if (prevVisible || isConsoleVisible
) postScreenRepaintDelayed();
1941 vbwin
.addEventListener((HideMouseEvent evt
) {
1942 if (vbwin
.closed
) return;
1943 if (isQuitRequested
) { vbwin
.close(); return; }
1944 if (!repostHideMouse
) {
1945 if (mainPane
!is null) mainPane
.lastUrlIndex
= -1;
1947 vbwin
.redrawOpenGlSceneNow();
1951 vbwin
.addEventListener((ScreenRebuildEvent evt
) {
1952 if (vbwin
.closed
) return;
1953 if (isQuitRequested
) { vbwin
.close(); return; }
1955 vbwin
.redrawOpenGlSceneNow();
1956 if (isConsoleVisible
) postScreenRepaintDelayed();
1959 vbwin
.addEventListener((ScreenRepaintEvent evt
) {
1960 if (vbwin
.closed
) return;
1961 if (isQuitRequested
) { vbwin
.close(); return; }
1962 vbwin
.redrawOpenGlSceneNow();
1963 if (isConsoleVisible
) postScreenRepaintDelayed();
1966 vbwin
.addEventListener((CursorBlinkEvent evt
) {
1967 if (vbwin
.closed
) return;
1969 vbwin
.redrawOpenGlSceneNow();
1972 vbwin
.addEventListener((QuitEvent evt
) {
1973 if (vbwin
.closed
) return;
1974 if (isQuitRequested
) { vbwin
.close(); return; }
1979 vbwin
.addEventListener((TrayStartAnimationEvent evt
) {
1980 if (vbwin
.closed
) return;
1981 if (isQuitRequested
) { vbwin
.close(); return; }
1982 trayStartAnimation();
1985 vbwin
.addEventListener((TrayStopAnimationEvent evt
) {
1986 if (vbwin
.closed
) return;
1987 if (isQuitRequested
) { vbwin
.close(); return; }
1988 trayStopAnimation();
1991 vbwin
.addEventListener((TraySetupAnimationEvent evt
) {
1992 if (vbwin
.closed
) return;
1993 if (isQuitRequested
) { vbwin
.close(); return; }
1994 setupTrayAnimation();
1997 vbwin
.addEventListener((TrayAnimationStepEvent evt
) {
1998 if (vbwin
.closed
) return;
1999 if (isQuitRequested
) { vbwin
.close(); return; }
2000 trayDoAnimationStep();
2003 HintWindow uphintWindow
;
2005 vbwin
.addEventListener((UpdatingAccountEvent evt
) {
2006 if (evt
.accName
.length
) {
2007 if (uphintWindow
!is null) {
2008 uphintWindow
.message
= "updating: "~evt
.accName
;
2010 uphintWindow
= new HintWindow("updating: "~evt
.accName
);
2011 uphintWindow
.winy
= guiThreadListHeight
+1+(3*gxTextHeightUtf
+2-uphintWindow
.winh
)/2;
2013 postScreenRebuild();
2017 vbwin
.addEventListener((UpdatingAccountCompleteEvent evt
) {
2018 if (evt
.accName
.length
&& uphintWindow
!is null) {
2019 uphintWindow
.message
= "done: "~evt
.accName
;
2020 postScreenRebuild();
2024 vbwin
.addEventListener((UpdatingCompleteEvent evt
) {
2026 uphintWindow
.close();
2027 uphintWindow
= null;
2029 foreach (Folder
fld; folders
) if (fld.needRebuild
) fld.buildVisibleList();
2030 setupTrayAnimation(); // check if we have to start/stop animation, and do it
2032 vbwin
.redrawOpenGlSceneNow();
2035 vbwin
.addEventListener((DoCheckBoxesCycleEvent evt
) {
2036 //conwriteln("updating group '", groups[evt.gidx].mbase.groupname);
2037 if (!atomicLoad(updateInProgress
)) {
2038 updateThreadId
.send(UpThreadCommand
.StartUpdate
);
2042 vbwin
.addEventListener((ArticleTextScrollEvent evt
) {
2043 if (vbwin
is null || vbwin
.closed
) return;
2044 if (mainPane
is null) return;
2045 mainPane
.doScrollStep();
2048 vbwin
.redrawOpenGlScene
= delegate () {
2049 if (vbwin
.closed
) return;
2051 bool resizeWin
= false;
2052 bool rebuildTexture
= false;
2056 scope(exit
) consoleUnlock();
2058 if (!conQueueEmpty()) postDoConCommands();
2060 if (VBufScale
!= vbufEffScale
) {
2061 // window scale changed
2062 vbufEffScale
= VBufScale
;
2065 if (vbufEffVSync
!= vbufVSync
) {
2066 vbufEffVSync
= vbufVSync
;
2067 vbwin
.vsync
= vbufEffVSync
;
2072 vbwin
.resize(winWidthScaled
, winHeightScaled
);
2073 glconResize(winWidthScaled
, winHeightScaled
);
2074 rebuildTexture
= true;
2077 if (rebuildTexture
) repaintScreen();
2079 glMatrixMode(GL_PROJECTION
); // for ortho camera
2081 // left, right, bottom, top, near, far
2082 //glViewport(0, 0, w*vbufEffScale, h*vbufEffScale);
2083 //glOrtho(0, w, h, 0, -1, 1); // top-to-bottom
2084 glViewport(0, 0, VBufWidth
*vbufEffScale
, VBufHeight
*vbufEffScale
);
2085 glOrtho(0, VBufWidth
, VBufHeight
, 0, -1, 1); // top-to-bottom
2086 glMatrixMode(GL_MODELVIEW
);
2089 glEnable(GL_TEXTURE_2D
);
2090 glDisable(GL_LIGHTING
);
2091 glDisable(GL_DITHER
);
2092 //glDisable(GL_BLEND);
2093 glDisable(GL_DEPTH_TEST
);
2094 //glEnable(GL_BLEND);
2095 //glBlendFunc(GL_SRC_ALPHA, GL_ONE);
2096 //glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
2097 glDisable(GL_BLEND
);
2098 //glDisable(GL_STENCIL_TEST);
2101 immutable w
= VBufWidth
;
2102 immutable h
= VBufHeight
;
2104 glColor4f(1, 1, 1, 1);
2105 glBindTexture(GL_TEXTURE_2D
, vglTexId
);
2106 //scope(exit) glBindTexture(GL_TEXTURE_2D, 0);
2108 glTexCoord2f(0.0f, 0.0f); glVertex2i(0, 0); // top-left
2109 glTexCoord2f(1.0f, 0.0f); glVertex2i(w
, 0); // top-right
2110 glTexCoord2f(1.0f, 1.0f); glVertex2i(w
, h
); // bottom-right
2111 glTexCoord2f(0.0f, 1.0f); glVertex2i(0, h
); // bottom-left
2115 if (vArrowTextureId
) {
2116 if (isMouseVisible
) {
2117 int px
= lastMouseX
;
2118 int py
= lastMouseY
;
2120 //glBlendFunc(GL_SRC_ALPHA, GL_ONE);
2121 glBlendFunc(GL_SRC_ALPHA
, GL_ONE_MINUS_SRC_ALPHA
);
2122 glColor4f(1, 1, 1, 1);
2123 glBindTexture(GL_TEXTURE_2D
, vArrowTextureId
);
2124 //scope(exit) glBindTexture(GL_TEXTURE_2D, 0);
2126 glTexCoord2f(0.0f, 0.0f); glVertex2i(px
, py
); // top-left
2127 glTexCoord2f(1.0f, 0.0f); glVertex2i(px
+16, py
); // top-right
2128 glTexCoord2f(1.0f, 1.0f); glVertex2i(px
+16, py
+8); // bottom-right
2129 glTexCoord2f(0.0f, 1.0f); glVertex2i(px
, py
+8); // bottom-left
2136 if (isQuitRequested()) vbwin
.postEvent(new QuitEvent());
2139 static if (is(typeof(&vbwin
.closeQuery
))) {
2140 vbwin
.closeQuery
= delegate () { concmd("quit"); postDoConCommands(); };
2143 vbwin
.visibleForTheFirstTime
= delegate () {
2145 vbwin
.setAsCurrentOpenGlContext();
2146 vbufEffVSync
= vbufVSync
;
2147 vbwin
.vsync
= vbufEffVSync
;
2149 // initialize OpenGL texture
2151 enum wrapOpt
= GL_REPEAT
;
2152 enum filterOpt
= GL_NEAREST
; //GL_LINEAR;
2153 enum ttype
= GL_UNSIGNED_BYTE
;
2155 glGenTextures(1, &vglTexId
);
2156 if (vglTexId
== 0) assert(0, "can't create OpenGL texture");
2158 //GLint gltextbinding;
2159 //glGetIntegerv(GL_TEXTURE_BINDING_2D, &gltextbinding);
2160 //scope(exit) glBindTexture(GL_TEXTURE_2D, gltextbinding);
2162 glBindTexture(GL_TEXTURE_2D
, vglTexId
);
2163 glTexParameterf(GL_TEXTURE_2D
, GL_TEXTURE_WRAP_S
, wrapOpt
);
2164 glTexParameterf(GL_TEXTURE_2D
, GL_TEXTURE_WRAP_T
, wrapOpt
);
2165 glTexParameteri(GL_TEXTURE_2D
, GL_TEXTURE_MIN_FILTER
, filterOpt
);
2166 glTexParameteri(GL_TEXTURE_2D
, GL_TEXTURE_MAG_FILTER
, filterOpt
);
2167 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
2168 //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
2170 GLfloat
[4] bclr
= 0.0;
2171 glTexParameterfv(GL_TEXTURE_2D
, GL_TEXTURE_BORDER_COLOR
, bclr
.ptr
);
2173 glTexImage2D(GL_TEXTURE_2D
, 0, GL_RGBA
, VBufWidth
, VBufHeight
, 0, GLTexType
, GL_UNSIGNED_BYTE
, vglTexBuf
);
2176 createArrowTexture();
2178 glconInit(winWidthScaled
, winHeightScaled
);
2181 vbwin
.redrawOpenGlSceneNow();
2183 updateThreadId
= spawn(&updateThread
, thisTid
);
2185 // create notification icon
2186 if (trayicon
is null) {
2187 auto drv
= vfsAddPak(wrapMemoryRO(iconsZipData
[]), "", "databinz/icons.zip:");
2188 scope(exit
) vfsRemovePak(drv
);
2190 foreach (immutable idx
; 0..6) {
2191 string fname
= "databinz/icons.zip:icons";
2193 fname
~= "/main.png";
2195 import std
.format
: format
;
2196 fname
= "%s/bat%s.png".format(fname
, idx
-1);
2198 auto fl
= VFile(fname
);
2199 if (fl
.size
== 0 || fl
.size
> 1024*1024) throw new Exception("fucked icon");
2200 auto pngraw
= new ubyte[](cast(uint)fl
.size
);
2201 fl
.rawReadExact(pngraw
);
2202 auto img
= readPng(pngraw
);
2203 if (img
is null) throw new Exception("fucked icon");
2204 icons
[idx
] = imageFromPng(img
);
2206 foreach (immutable idx
, MemoryImage img
; icons
[]) {
2207 trayimages
[idx
] = Image
.fromMemoryImage(img
);
2209 vbwin
.icon
= icons
[0];
2210 trayicon
= new NotificationAreaIcon("Chiroptera", trayimages
[0], (MouseButton button
) {
2211 scope(exit
) if (!conQueueEmpty()) postDoConCommands();
2212 if (button
== MouseButton
.left
) vbwin
.switchToWindow();
2213 if (button
== MouseButton
.middle
) concmd("quit");
2215 setupTrayAnimation();
2216 flushGui(); // or it may not redraw itself
2217 } catch (Exception e
) {
2218 conwriteln("ERROR loading icons: ", e
.msg
);
2223 mainPane
= new MainPaneWindow();
2225 postScreenRebuild();
2228 vbwin
.eventLoop(1000*10,
2230 scope(exit
) if (!conQueueEmpty()) postDoConCommands();
2231 if (vbwin
.closed
) return;
2234 scope(exit
) consoleUnlock();
2237 if (isQuitRequested
) { vbwin
.close(); return; }
2239 delegate (KeyEvent event
) {
2240 scope(exit
) if (!conQueueEmpty()) postDoConCommands();
2241 if (vbwin
.closed
) return;
2242 if (isQuitRequested
) { vbwin
.close(); return; }
2243 if (glconKeyEvent(event
)) {
2244 postScreenRepaint();
2247 //if (event.pressed) ignoreSubWinChar = false;
2248 if (dispatchEvent(event
)) return;
2249 //postScreenRepaint(); // just in case
2251 delegate (MouseEvent event
) {
2252 scope(exit
) if (!conQueueEmpty()) postDoConCommands();
2253 if (vbwin
.closed
) return;
2254 lastMouseXUnscaled
= event
.x
;
2255 lastMouseYUnscaled
= event
.y
;
2256 if (event
.type
== MouseEventType
.buttonPressed
) lastMouseButton |
= event
.button
;
2257 else if (event
.type
== MouseEventType
.buttonReleased
) lastMouseButton
&= ~event
.button
;
2259 if (dispatchEvent(event
)) return;
2261 delegate (dchar ch
) {
2262 if (vbwin
.closed
) return;
2263 scope(exit
) if (!conQueueEmpty()) postDoConCommands();
2264 if (glconCharEvent(ch
)) {
2265 postScreenRepaint();
2268 if (dispatchEvent(ch
)) return;
2271 saveCurrentFolderAndPosition();
2272 trayimages
[] = null;
2273 if (trayicon
!is null && !trayicon
.closed
) { trayicon
.close(); trayicon
= null; }
2275 updateThreadId
.send(UpThreadCommand
.Quit
);
2276 conProcessQueue(int.max
/4);