using GxRect and GxPoint for some coord manupulation (and clipping rectangle)
[chiroptera.git] / chiroptera.d
blobb5effb107990e2938cd2c91d9e857c847f918a91
1 /* E-Mail Client
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;
20 import core.atomic;
21 import core.time;
22 import std.concurrency;
24 //import arsd.email;
25 //import arsd.htmltotext;
26 import arsd.simpledisplay;
27 import arsd.png;
29 import iv.bclamp;
30 import iv.encoding;
31 import iv.cmdcon;
32 import iv.cmdcongl;
33 import iv.lockfile;
34 import iv.sdpyutil;
35 import iv.strex;
36 import iv.utfutil;
37 import iv.vfs.io;
39 import maildb;
40 import folder;
41 import account;
42 import hitwit;
44 import addrbook;
46 import egfx;
47 import egui;
48 import editor;
49 import egui_dialogs;
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;
65 } else {
66 browser = "opera";
69 return browser;
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;
101 } else {
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; }
144 trayStopAnimation();
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);
158 if (concmd.length) {
159 mainAppKeyBindings[evt.toStr] = concmd.idup;
160 } else {
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 {
212 Ping,
213 StartUpdate, // start updating now
214 Quit,
217 void updateThread (Tid ownerTid) {
218 import core.time;
219 bool doQuit = false;
220 try {
221 MonoTime lastCollect = MonoTime.currTime;
222 while (!doQuit) {
223 receiveTimeout(30.seconds,
224 (UpThreadCommand cmd) {
225 final switch (cmd) {
226 case UpThreadCommand.Ping: break;
227 case UpThreadCommand.StartUpdate: break;
228 case UpThreadCommand.Quit: doQuit = true; break;
232 if (doQuit) break;
233 bool updateProgressSet = false;
234 foreach (Account acc; accounts) {
235 if (acc.needUpdate) {
236 updateProgressSet = true;
237 atomicStore(updateInProgress, true);
238 try {
239 if (vbwin !is null) vbwin.postEvent(new UpdatingAccountEvent(acc.name));
240 acc.update();
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;
255 lastCollect = ctt;
256 GC.collect();
257 GC.minimize();
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) {
286 vbufVSync = nv;
287 postScreenRepaint();
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 // //////////////////////////////////////////////////////////////////// //
299 conRegFunc!(() {
300 import core.memory : GC;
301 conwriteln("starting GC collection...");
302 GC.collect();
303 GC.minimize();
304 conwriteln("GC collection complete.");
305 })("gc_collect", "force GC collection cycle");
308 // //////////////////////////////////////////////////////////////////// //
309 conRegFunc!(() {
310 auto qww = new YesNoWindow("Quit?", "Do you really want to quit?", true);
311 qww.onYes = () { concmd("quit"); };
312 qww.addModal();
313 })("quit_prompt", "quit with prompt");
316 // //////////////////////////////////////////////////////////////////// //
317 conRegFunc!((ConString url) {
318 if (url.length) {
319 import std.stdio : File;
320 import std.process;
321 auto pid = spawnProcess(
322 [getBrowserCommand, url.idup],
323 File("/dev/zero"), File("/dev/null"), File("/dev/null"),
325 pid.wait();
327 })("open_url", "open given url in a browser");
330 // //////////////////////////////////////////////////////////////////// //
331 conRegFunc!(() {
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 // //////////////////////////////////////////////////////////////////// //
340 conRegFunc!(() {
341 if (folderCur > 0) folderCur = folderCur-1;
342 postScreenRebuild();
343 })("folder_prev", "go to previous group");
345 conRegFunc!(() {
346 if (folders.length-folderCur > 1) folderCur = folderCur+1;
347 postScreenRebuild();
348 })("folder_next", "go to next group");
351 // //////////////////////////////////////////////////////////////////// //
352 conRegFunc!(() {
353 if (auto fld = getActiveFolder) {
354 if (fld.markAsUnread()) postScreenRebuild();
356 })("mark_unread", "mark current message as unread");
358 conRegFunc!(() {
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;
368 // try other folders
369 uint fidx = cast(uint)((folderCur+1)%folders.length);
370 foreach (immutable _; 0..folders.length) {
371 if (folders[fidx].moveToNextUnread(true)) {
372 folderCur = fidx;
373 postScreenRebuild();
374 return;
376 fidx = cast(uint)((fidx+1)%folders.length);
378 return;
380 postScreenRebuild();
382 })("next_unread", "move to next unread message; bool arg: can skip to next group?");
385 // //////////////////////////////////////////////////////////////////// //
386 conRegFunc!(() {
387 if (mainPane !is null) mainPane.scrollByPageUp();
388 })("artext_page_up", "do pageup on article text");
390 conRegFunc!(() {
391 if (mainPane !is null) mainPane.scrollByPageDown();
392 })("artext_page_down", "do pagedown on article text");
394 conRegFunc!(() {
395 if (mainPane !is null) mainPane.scrollBy(-1);
396 })("artext_line_up", "do lineup on article text");
398 conRegFunc!(() {
399 if (mainPane !is null) mainPane.scrollBy(1);
400 })("artext_line_down", "do linedown on article text");
402 // //////////////////////////////////////////////////////////////////// //
403 conRegFunc!(() {
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;
414 import std.process;
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"),
422 pid.wait();
428 })("article_open_in_browser", "open the current article in browser");
430 conRegFunc!(() {
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 // //////////////////////////////////////////////////////////////////// //
452 conRegFunc!(() {
453 if (auto fld = getActiveFolder) {
454 fld.moveUp();
455 postScreenRebuild();
457 })("article_prev", "go to previous article");
459 conRegFunc!(() {
460 if (auto fld = getActiveFolder) {
461 fld.moveDown();
462 postScreenRebuild();
464 })("article_next", "go to next article");
466 conRegFunc!(() {
467 if (auto fld = getActiveFolder) {
468 fld.movePageUp();
469 postScreenRebuild();
471 })("article_pgup", "artiles list: page up");
473 conRegFunc!(() {
474 if (auto fld = getActiveFolder) {
475 fld.movePageDown();
476 postScreenRebuild();
478 })("article_pgdown", "artiles list: page down");
480 conRegFunc!(() {
481 if (auto fld = getActiveFolder) {
482 fld.scrollUp();
483 postScreenRebuild();
485 })("article_scroll_up", "scroll article list up");
487 conRegFunc!(() {
488 if (auto fld = getActiveFolder) {
489 fld.scrollDown();
490 postScreenRebuild();
492 })("article_scroll_down", "scroll article list up");
494 conRegFunc!(() {
495 if (auto fld = getActiveFolder) {
496 fld.moveToFirst();
497 postScreenRebuild();
499 })("article_to_first", "go to first article");
501 conRegFunc!(() {
502 if (auto fld = getActiveFolder) {
503 fld.moveToLast();
504 postScreenRebuild();
506 })("article_to_last", "go to last article");
509 // //////////////////////////////////////////////////////////////////// //
510 conRegFunc!(() {
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;
522 } else {
523 pw.title = "Reply from '"~acc.name~"' ("~acc.mail~")";
524 pw.to.str = "";
525 pw.activeWidget = pw.to;
527 pw.subj.str = "";
528 pw.acc = acc;
529 pw.fld = fld;
532 auto acc = fld.findAccountToPost();
533 if (acc is null) {
534 auto wacc = new SelectPopBoxWindow(defaultAcc);
535 wacc.onSelected = postDg;
536 } else {
537 postDg(acc);
538 acc = defaultAcc;
540 } else {
541 conwriteln("post: no active folder");
543 })("new_post", "post a new article or message");
546 // //////////////////////////////////////////////////////////////////// //
547 conRegFunc!(() {
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;
565 } else {
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-");
574 if (vp > 0) {
575 from = from[0..vp].xstrip;
576 if (from.length == 0) from = "anonymous";
580 pw.ed.addText(from);
581 pw.ed.addText(" wrote:\n");
582 pw.ed.addText("\n");
583 foreach (ConString s; LineIterator!false(atext)) {
584 if (s.length == 0 || s[0] == '>') pw.ed.addText(">"); else pw.ed.addText("> ");
585 pw.ed.addText(s);
586 pw.ed.addText("\n");
588 pw.ed.addText("\n");
589 pw.ed.reformat();
590 pw.replyto = art.msgid;
591 pw.references = art.getHeaderValue("References").xstrip.idup;
592 pw.acc = acc;
593 pw.fld = fld;
594 pw.activeWidget = pw.ed;
600 auto acc = fld.findAccountToPost(fld.curidx);
601 if (acc is null) {
602 auto wacc = new SelectPopBoxWindow(defaultAcc);
603 wacc.onSelected = postDg;
604 } else {
605 postDg(acc);
606 acc = defaultAcc;
609 })("article_reply", "reply to the current article");
612 // //////////////////////////////////////////////////////////////////// //
613 conRegFunc!(() {
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");
630 conRegFunc!(() {
631 if (auto fld = getActiveFolder) {
632 Article newart;
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();
646 didx = aidx;
651 // delete it
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();
662 // insert into spam
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();
674 postScreenRebuild();
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; }
685 Article newart;
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();
694 didx = aidx;
698 if (didx == uint.max || newart is null) { conwriteln("article not found!"); return; }
699 // delete it
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();
709 // insert into spam
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();
718 postScreenRebuild();
720 })("article_move_to_folder", "move article to existing folder");
723 // //////////////////////////////////////////////////////////////////// //
724 conRegFunc!(() {
725 if (auto fld = getActiveFolder) {
726 fld.moveToParent();
727 postScreenRebuild();
729 })("article_to_parent", "jump to parent article, if any");
731 conRegFunc!(() {
732 if (auto fld = getActiveFolder) {
733 fld.moveToPrevSib();
734 postScreenRebuild();
736 })("article_to_prev_sib", "jump to previous sibling");
738 conRegFunc!(() {
739 if (auto fld = getActiveFolder) {
740 fld.moveToNextSib();
741 postScreenRebuild();
743 })("article_to_next_sib", "jump to next sibling");
746 // //////////////////////////////////////////////////////////////////// //
747 conRegFunc!(() {
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) {
755 string xcmd;
756 if (t is null) {
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);
760 } else {
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);
763 } else {
764 concmdfex!"twit_set name \"%s\" mail \"%s\" folder_mask \"%s\" title \"%s\""((ConString cmd) { xcmd = cmd.xstrip.idup; }, name, mail, folder, title);
767 try {
768 import std.path : buildPath;
769 auto fo = VFile(buildPath(mailRootDir, "auto_twits.rc"), "a");
770 fo.writeln(xcmd);
771 } catch (Exception e) {
772 conwriteln("ERROR writing twit info: ", e.msg);
775 // HACK: FIXME!
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 // //////////////////////////////////////////////////////////////////// //
787 conRegFunc!(() {
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
799 postScreenRebuild();
801 })("article_twit_thread", "twit current thread");
803 conRegFunc!(() {
804 if (auto fld = getActiveFolder) {
805 if (fld.toggleSoftDeleted()) postScreenRebuild();
807 })("article_softdelete_toggle", "toggle \"soft deleted\" flag on current article");
810 // //////////////////////////////////////////////////////////////////// //
811 conRegFunc!(() {
812 if (auto fld = getActiveFolder) {
813 if (fld.curidxValid) {
814 fld.withBaseReader(delegate (abase, cur, top, alist) {
815 auto art = abase[alist[cur]];
816 if (art !is null) {
817 conwriteln("============================");
818 abase.loadContent(alist[cur]);
819 scope(exit) art.releaseContent();
820 ConString from;
821 foreach (ConString s; art.headersIterator!false) {
822 conwriteln(" ", s);
823 if (from.length == 0 && s.startsWithCI("From:")) from = s;
825 conwriteln("---------------");
826 conwriteln(" ", art.fromname, " <", art.frommail, ">");
827 conwriteln(" ", decodeq(from));
828 conwrite(" ");
829 Utf8DecoderFast dc;
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);
836 conwriteln();
841 })("article_dump_headers", "dump article headers");
843 conRegFunc!(() {
844 if (auto fld = getActiveFolder) {
845 fld.rebuildIndex();
846 postScreenRebuild();
848 })("folder_rebuild_index", "rebuild index file");
850 conRegFunc!(() {
851 if (auto fld = getActiveFolder) {
852 fld.packText();
853 postScreenRebuild();
855 })("folder_pack_textdb", "pack (rebuild) text database for current folder");
857 conRegVar("folder_hide_old", "should old articles be hidden in UI?",
858 delegate (self) {
859 if (auto fld = getActiveFolder) return fld.hideOldThreads;
860 return false;
862 delegate (self, bool nv) {
863 if (auto fld = getActiveFolder) fld.hideOldThreads = nv;
868 conRegFunc!(() {
869 if (auto fld = getActiveFolder) {
870 fld.withBase(delegate (abase) {
871 uint idx = fld.curidx;
872 if (idx >= fld.length) {
873 idx = 0;
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)];
881 if (art !is null) {
882 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
883 fld.curidx = cast(int)idx;
884 postScreenRebuild();
885 return;
888 idx = (idx+1)%fld.length;
892 })("find_mine", "find mine article");
895 // //////////////////////////////////////////////////////////////////// //
896 conRegFunc!(() {
897 new FontWindow();
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();
912 try {
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);
917 if (fname.isDir) {
918 string res = fname.idup;
919 if (res.length && res[$-1] != '/') res ~= '/';
920 res ~= Article.fixAttachmentName(attname);
921 return res;
923 return fname.idup;
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;
960 this () {
961 super(null, 0, 0, VBufWidth, VBufHeight);
962 mType = Type.OnBottom;
963 add();
966 // //////////////////////////////////////////////////////////////////// //
967 static struct WebLink {
968 int ly; // in lines
969 int x, len; // in pixels
970 string url;
971 string text; // visual text
972 int attachnum = -1;
974 @property bool isAttach () const pure nothrow @safe @nogc { return (attachnum >= 0); }
977 WebLink[] emurls;
978 int lastUrlIndex = -1;
980 void clearDecodedText () {
981 emurls[] = WebLink.init;
982 emlines[] = null;
983 emurls.length = 0;
984 emurls.assumeSafeAppend;
985 emlines.length = 0;
986 emlines.assumeSafeAppend;
987 lastDecodedMsgFolderPath = null;
988 lastDecodedMsgId = null;
989 articleTextTopLine = 0;
990 articleDestTextTopLine = 0;
991 lastUrlIndex = -1;
994 // <0: not on url
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;
1006 mx -= tpX0;
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) {
1013 if (wl.ly == idx) {
1014 if (mx >= wl.x && mx < wl.x+wl.len) return cast(int)uidx;
1018 ++idx;
1019 y += gxTextHeightUtf;
1022 return -1;
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" ];
1033 lastUrlIndex = -1;
1035 static sptrdiff urlepos (const(char)[] s, sptrdiff spos) {
1036 assert(spos < s.length);
1037 spos = s.indexOf("://", spos);
1038 assert(spos >= 0);
1039 spos += 3;
1040 // host
1041 while (spos < s.length) {
1042 char ch = s[spos];
1043 if (ch == '/') break;
1044 if (ch <= ' ') return spos;
1045 if (ch != '.' && ch != '-' && !ch.isalnum) return spos;
1046 ++spos;
1048 if (spos >= s.length) return spos;
1049 // path
1050 assert(s[spos] == '/');
1051 while (spos < s.length) {
1052 char ch = s[spos];
1053 if (ch <= ' ' || ch == '<' || ch == '>') return spos;
1054 if (ch == '.' || ch == ')' || ch == '-' || ch == '!' || ch == ';') {
1055 if (s.length-spos > 1) {
1056 ch = s[spos+1];
1057 if (ch <= ' ' || ch == '.' || ch == '(' || ch == ')' || ch == '-' || ch == '!' || ch == ';' || ch == '<' || ch == '>') return spos;
1060 ++spos;
1062 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("://");
1069 while (pos > 0) {
1070 bool found = false;
1071 auto spos = pos;
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) {
1075 found = true;
1076 spos -= proto.length;
1077 break;
1081 if (found) {
1082 // find URL end
1083 auto epos = urlepos(s, spos);
1084 //conwriteln("spos=", spos, "; epos=", epos, "; s=[", s, "]; link=[", s[spos..epos], "]");
1085 WebLink wl;
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;
1091 emurls ~= wl;
1092 pos = epos;
1093 } else {
1094 ++pos;
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;
1107 auto epos = spos+7;
1108 while (epos < s.length && s.ptr[epos] > ' ') ++epos;
1109 //if (attachcount >= parts.length) break;
1110 WebLink wl;
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];
1119 ++attachcount;
1120 emurls ~= wl;
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;
1137 clearDecodedText();
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); }
1144 int lastQLevel = 0;
1146 void putLine (ConString s) {
1147 enum QuoteStr = ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ";
1148 static string addQuotes(T:const(char)[]) (T s, int qlevel) {
1149 if (qlevel <= 0) {
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
1156 int qlevel = 0;
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;
1162 } else {
1163 lastqpos = pos;
1164 ++qlevel;
1166 ++pos;
1168 if (s.length-lastqpos > 1 && s.ptr[lastqpos+1] == ' ') ++lastqpos;
1169 ++lastqpos;
1170 s = s[lastqpos..$];
1172 //conwriteln("qlevel=", qlevel, "; s=[", s, "]");
1173 // empty line: just insert it
1174 if (s.length == 0) {
1175 emlines ~= addQuotes(null, qlevel).xstripright;
1176 } else {
1177 // can we append?
1178 bool newline = false;
1179 if (lastQLevel != qlevel || !lastEndsWithSpace) {
1180 newline = true;
1181 } else {
1182 // append words
1183 //while (s.length > 1 && s.ptr[0] <= ' ') s = s[1..$];
1185 while (s.length) {
1186 usize epos = 0;
1187 if (newline) {
1188 while (epos < s.length && s.ptr[epos] <= ' ') ++epos;
1189 } else {
1190 //assert(s[0] > ' ');
1191 while (epos < s.length && s.ptr[epos] <= ' ') ++epos;
1193 while (epos < s.length && s.ptr[epos] > ' ') ++epos;
1194 auto xlen = 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];
1199 } else {
1200 newline = false;
1201 // wrapping; new line
1202 emlines ~= addQuotes(s[0..epos], qlevel);
1204 s = s[epos..$];
1206 if (newline) emlines ~= addQuotes(null, qlevel);
1208 lastQLevel = qlevel;
1211 try {
1212 foreach (ConString s; LineIterator!false(art.getTextContent)) {
1213 static if (doLocalConvert) {
1214 putLine(s.uniRecode);
1215 } else {
1216 putLine(s);
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);
1229 // attaches
1230 auto lcount = cast(uint)emlines.length;
1231 emlinesBeforeAttaches = lcount;
1233 static if (doLocalConvert) {
1234 uint attcount = 0;
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);
1240 emlines ~= s;
1241 ++attcount;
1242 return false;
1244 emlDetectUrls(lcount);
1245 } else {
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;
1260 } else {
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;
1271 foreach (; 0..6) {
1272 normalizeArticleTopLine();
1273 if (articleDestTextTopLine < articleTextTopLine) {
1274 --articleTextTopLine;
1275 } else if (articleDestTextTopLine > articleTextTopLine) {
1276 ++articleTextTopLine;
1277 } else {
1278 break;
1280 normalizeArticleTopLine();
1282 if (articleTextTopLine == oldtop) {
1283 // can't scroll anymore
1284 articleDestTextTopLine = articleTextTopLine;
1285 return;
1287 postScreenRebuild();
1288 postArticleScroll();
1291 void scrollBy (int delta) {
1292 articleDestTextTopLine += delta;
1293 doScrollStep();
1296 void scrollByPageUp () {
1297 int lines = visibleArticleLines-1;
1298 if (lines < 1) lines = 1;
1299 scrollBy(-lines);
1302 void scrollByPageDown () {
1303 int lines = visibleArticleLines-1;
1304 if (lines < 1) lines = 1;
1305 scrollBy(lines);
1308 // //////////////////////////////////////////////////////////////////// //
1309 //TODO: move parts to widgets
1310 override void onPaint () {
1311 clipReset();
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;
1324 char[128] tbuf;
1325 const(char)[] tbufs;
1327 void xfmt (string s, const(char)[][] strs...) {
1328 int dpos = 0;
1329 void puts (const(char)[] s...) {
1330 foreach (char ch; s) {
1331 if (dpos >= tbuf.length) break;
1332 tbuf[dpos++] = ch;
1335 while (s.length) {
1336 if (strs.length && s.length > 1 && s[0] == '%' && s[1] == 's') {
1337 puts(strs[0]);
1338 strs = strs[1..$];
1339 s = s[2..$];
1340 } else {
1341 puts(s[0]);
1342 s = s[1..$];
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;
1366 // header
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));
1381 // text
1382 int y = clipRect.y0+3*gxTextHeightUtf+2;
1383 immutable sty = y;
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) {
1393 int qlevel = 0;
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;
1400 ++qlevel;
1403 clr = gxRGB!( 0, 128+40, 0);
1404 if (qlevel) {
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) {
1414 if (wl.ly == idx) {
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;
1425 ++idx;
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;
1463 clipRect.y0 = 0;
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;
1469 int y = 0;
1470 //conwriteln(fld.msgtop, " : ", fld.list.length);
1471 uint idx = top;
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);
1485 if (idx == cur) {
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;
1515 char[128] tmpbuf;
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-");
1524 if (vp > 0) {
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);
1546 ++idx;
1547 y += gxTextHeightUtf;
1550 // draw progressbar
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();
1565 int ofsx = 2;
1566 int ofsy = 1;
1567 foreach (immutable idx, Folder fld; folders) {
1568 if (idx < folderTop) continue;
1569 if (ofsy >= VBufHeight) break;
1570 clipReset();
1571 if (idx == folderCur) gxFillRect(0, ofsy-1, guiGroupListWidth, (gxTextHeightUtf+2), gxRGB!(0, 127, 127));
1572 clipRect.x0 = ofsx-1;
1573 clipRect.y0 = ofsy;
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);
1579 } else {
1580 if (depth == 0) {
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);
1599 char[64] kname;
1600 if (auto cmdp = event.toStrBuf(kname[]) in mainAppKeyBindings) {
1601 concmd(*cmdp);
1602 return true;
1604 // debug
1606 if (event == "S-Up") {
1607 if (folderTop > 0) --folderTop;
1608 postScreenRebuild();
1609 return true;
1611 if (event == "S-Down") {
1612 if (folderTop+1 < folders.length) ++folderTop;
1613 postScreenRebuild();
1614 return true;
1617 //if (event == "Tab") { new PostWindow(); return true; }
1618 //if (event == "Tab") { new SelectPopBoxWindow(null); return true; }
1620 return false;
1623 // returning `false` to avoid screen rebuilding by dispatcher
1624 override bool onMouse (MouseEvent event) {
1625 //FIXME: use window coordinates
1626 int mx, my;
1627 event.mouse2xy(mx, my);
1628 // button press
1629 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
1630 // select folder
1631 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < VBufHeight) {
1632 uint fnum = my/(gxTextHeightUtf+2)+folderTop;
1633 if (fnum != folderCur) {
1634 folderCur = fnum;
1635 postScreenRebuild();
1637 return false;
1639 // select post
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();
1645 return false;
1649 // wheel
1650 if (event.type == MouseEventType.buttonPressed && (event.button == MouseButton.wheelUp || event.button == MouseButton.wheelDown)) {
1651 // folder
1652 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < VBufHeight) {
1653 if (event.button == MouseButton.wheelUp) {
1654 if (folderCur > 0) folderCur = folderCur-1;
1655 } else {
1656 if (folderCur+1 < folders.length) folderCur = folderCur+1;
1658 postScreenRebuild();
1659 return false;
1661 // post
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();
1666 return false;
1669 // text
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();
1674 return false;
1677 // button release
1678 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
1679 // try url
1680 auto url = findUrlAt(mx, my);
1681 if (url !is null) {
1682 if (url.isAttach) {
1683 concmdf!"attach_save %s"(url.attachnum);
1684 } else {
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();
1696 } else {
1697 postScreenRepaint();
1700 return false;
1703 override bool onChar (dchar ch) {
1704 return false;
1709 // ////////////////////////////////////////////////////////////////////////// //
1710 void repaintScreen () {
1711 clipReset();
1712 gxClearScreen(0);
1713 paintSubWindows();
1714 vglUpdateTexture();
1718 // ////////////////////////////////////////////////////////////////////////// //
1719 class FontWindow : SubWindow {
1720 int cx, cy;
1722 this () {
1723 super("", 16*14+4, 16*14+4+10);
1724 //if (hasWindowClass(this)) return;
1725 add();
1728 override void onPaint () {
1729 import core.stdc.stdio : snprintf;
1730 char[64] buf;
1732 auto tlen = snprintf(buf.ptr, buf.length, "Font: \\x%02x (%d)", cast(int)(cy*16+cx), cast(int)(cy*16+cx));
1734 setupClip();
1735 gxDrawWindow(buf[0..tlen], gxRGB!(255, 255, 255), gxRGB!(0, 0, 0), gxRGB!(255, 255, 255), gxRGB!(0, 0, 180));
1737 setupClientClip();
1738 clipRect.x0 += 2;
1739 clipRect.y0 += 0;
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";
1784 initConsole();
1785 hitwitInitConsole();
1787 clearBindings();
1788 setupDefaultBindings();
1790 concmd("exec chiroptera.rc tan");
1792 scanFolders();
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);
1814 vbwin.hideCursor();
1816 vbwin.onFocusChange = delegate (bool focused) {
1817 vbfocused = focused;
1818 if (!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!");
1843 vglTexBuf = nv;
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;
1852 lastWinWidth = wdt;
1853 lastWinHeight = hgt;
1855 // reinitialize OpenGL texture
1857 import iv.glbinds;
1859 enum wrapOpt = GL_REPEAT;
1860 enum filterOpt = GL_NEAREST; //GL_LINEAR;
1861 enum ttype = GL_UNSIGNED_BYTE;
1863 if (vglTexId) glDeleteTextures(1, &vglTexId);
1864 vglTexId = 0;
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);
1886 mouseMoved();
1888 repaintScreen();
1889 vbwin.redrawOpenGlSceneNow();
1892 vbwin.addEventListener((DoConsoleCommandsEvent evt) {
1893 bool sendAnother = false;
1894 bool prevVisible = isConsoleVisible;
1896 consoleLock();
1897 scope(exit) consoleUnlock();
1898 conProcessQueue();
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;
1912 repaintScreen();
1913 vbwin.redrawOpenGlSceneNow();
1917 vbwin.addEventListener((ScreenRebuildEvent evt) {
1918 if (vbwin.closed) return;
1919 if (isQuitRequested) { vbwin.close(); return; }
1920 repaintScreen();
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;
1934 repaintScreen();
1935 vbwin.redrawOpenGlSceneNow();
1938 vbwin.addEventListener((QuitEvent evt) {
1939 if (vbwin.closed) return;
1940 if (isQuitRequested) { vbwin.close(); return; }
1941 vbwin.close();
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;
1981 } else {
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) {
1997 if (uphintWindow) {
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
2003 repaintScreen();
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;
2027 consoleLock();
2028 scope(exit) consoleUnlock();
2030 if (!conQueueEmpty()) postDoConCommands();
2032 if (VBufScale != vbufEffScale) {
2033 // window scale changed
2034 vbufEffScale = VBufScale;
2035 resizeWin = true;
2037 if (vbufEffVSync != vbufVSync) {
2038 vbufEffVSync = vbufVSync;
2039 vbwin.vsync = vbufEffVSync;
2043 if (resizeWin) {
2044 vbwin.resize(winWidthScaled, winHeightScaled);
2045 glconResize(winWidthScaled, winHeightScaled);
2046 rebuildTexture = true;
2049 if (rebuildTexture) repaintScreen();
2051 glMatrixMode(GL_PROJECTION); // for ortho camera
2052 glLoadIdentity();
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);
2059 glLoadIdentity();
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);
2072 if (vglTexId) {
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);
2079 glBegin(GL_QUADS);
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
2084 glEnd();
2087 if (vArrowTextureId) {
2088 if (isMouseVisible) {
2089 int px = lastMouseX;
2090 int py = lastMouseY;
2091 glEnable(GL_BLEND);
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);
2097 glBegin(GL_QUADS);
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
2102 glEnd();
2106 glconDraw();
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 () {
2116 import iv.glbinds;
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);
2152 repaintScreen();
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);
2161 try {
2162 foreach (immutable idx; 0..6) {
2163 string fname = "databinz/icons.zip:icons";
2164 if (idx == 0) {
2165 fname ~= "/main.png";
2166 } else {
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();
2198 repostHideMouse();
2200 vbwin.eventLoop(1000*10,
2201 delegate () {
2202 scope(exit) if (!conQueueEmpty()) postDoConCommands();
2203 if (vbwin.closed) return;
2205 consoleLock();
2206 scope(exit) consoleUnlock();
2207 conProcessQueue();
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();
2217 return;
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;
2233 default: break;
2235 } else {
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;
2248 mouseMoved();
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();
2256 return;
2258 if (dispatchEvent(ch)) return;
2261 saveCurrentFolderAndPosition();
2262 trayimages[] = null;
2263 if (trayicon !is null && !trayicon.closed) { trayicon.close(); trayicon = null; }
2264 flushGui();
2265 updateThreadId.send(UpThreadCommand.Quit);
2266 conProcessQueue(int.max/4);