mailedit: semi-working autowrapping
[chiroptera.git] / chiroptera.d
blob40c68213fbc3ca1e2efb9a132ce8f9cc018a3978
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("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 {
220 Ping,
221 StartUpdate, // start updating now
222 Quit,
225 void updateThread (Tid ownerTid) {
226 import core.time;
227 bool doQuit = false;
228 try {
229 MonoTime lastCollect = MonoTime.currTime;
230 while (!doQuit) {
231 receiveTimeout(30.seconds,
232 (UpThreadCommand cmd) {
233 final switch (cmd) {
234 case UpThreadCommand.Ping: break;
235 case UpThreadCommand.StartUpdate: break;
236 case UpThreadCommand.Quit: doQuit = true; break;
240 if (doQuit) break;
241 bool updateProgressSet = false;
242 foreach (Account acc; accounts) {
243 if (acc.needUpdate) {
244 updateProgressSet = true;
245 atomicStore(updateInProgress, true);
246 try {
247 if (vbwin !is null) vbwin.postEvent(new UpdatingAccountEvent(acc.name));
248 acc.update();
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;
263 lastCollect = ctt;
264 GC.collect();
265 GC.minimize();
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) {
294 vbufVSync = nv;
295 postScreenRepaint();
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 // //////////////////////////////////////////////////////////////////// //
307 conRegFunc!(() {
308 import core.memory : GC;
309 conwriteln("starting GC collection...");
310 GC.collect();
311 GC.minimize();
312 conwriteln("GC collection complete.");
313 })("gc_collect", "force GC collection cycle");
316 // //////////////////////////////////////////////////////////////////// //
317 conRegFunc!(() {
318 auto qww = new YesNoWindow("Quit?", "Do you really want to quit?", true);
319 qww.onYes = () { concmd("quit"); };
320 qww.addModal();
321 })("quit_prompt", "quit with prompt");
324 // //////////////////////////////////////////////////////////////////// //
325 conRegFunc!((ConString url) {
326 if (url.length) {
327 import std.stdio : File;
328 import std.process;
329 auto pid = spawnProcess(
330 [getBrowserCommand, url.idup],
331 File("/dev/zero"), File("/dev/null"), File("/dev/null"),
333 pid.wait();
335 })("open_url", "open given url in a browser");
338 // //////////////////////////////////////////////////////////////////// //
339 conRegFunc!(() {
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 // //////////////////////////////////////////////////////////////////// //
348 conRegFunc!(() {
349 if (folderCur > 0) folderCur = folderCur-1;
350 postScreenRebuild();
351 })("folder_prev", "go to previous group");
353 conRegFunc!(() {
354 if (folders.length-folderCur > 1) folderCur = folderCur+1;
355 postScreenRebuild();
356 })("folder_next", "go to next group");
359 // //////////////////////////////////////////////////////////////////// //
360 conRegFunc!(() {
361 if (auto fld = getActiveFolder) {
362 fld.markAsUnread();
363 setupTrayAnimation();
364 postScreenRebuild();
366 })("mark_unread", "mark current message as unread");
368 conRegFunc!(() {
369 if (auto fld = getActiveFolder) {
370 fld.markAsRead();
371 setupTrayAnimation();
372 postScreenRebuild();
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;
380 // try other folders
381 uint fidx = cast(uint)((folderCur+1)%folders.length);
382 foreach (immutable _; 0..folders.length) {
383 if (folders[fidx].moveToNextUnread(true)) {
384 folderCur = fidx;
385 setupTrayAnimation();
386 postScreenRebuild();
387 return;
389 fidx = cast(uint)((fidx+1)%folders.length);
391 return;
393 setupTrayAnimation();
394 postScreenRebuild();
396 })("next_unread", "move to next unread message; bool arg: can skip to next group?");
399 // //////////////////////////////////////////////////////////////////// //
400 conRegFunc!(() {
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;
405 postScreenRebuild();
408 if (mainPane !is null) mainPane.scrollByPageUp();
409 })("artext_page_up", "do pageup on article text");
411 conRegFunc!(() {
413 if (mainPane !is null) {
414 mainPane.articleTextTopLine += (VBufHeight-guiThreadListHeight-3*10-4)/gxTextHeightUtf;
415 postScreenRebuild();
418 if (mainPane !is null) mainPane.scrollByPageDown();
419 })("artext_page_down", "do pagedown on article text");
422 // //////////////////////////////////////////////////////////////////// //
423 conRegFunc!(() {
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;
434 import std.process;
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"),
442 pid.wait();
448 })("article_open_in_browser", "open the current article in browser");
450 conRegFunc!(() {
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 // //////////////////////////////////////////////////////////////////// //
472 conRegFunc!(() {
473 if (auto fld = getActiveFolder) {
474 fld.moveUp();
475 postScreenRebuild();
477 })("article_prev", "go to previous article");
479 conRegFunc!(() {
480 if (auto fld = getActiveFolder) {
481 fld.moveDown();
482 postScreenRebuild();
484 })("article_next", "go to next article");
486 conRegFunc!(() {
487 if (auto fld = getActiveFolder) {
488 fld.movePageUp();
489 postScreenRebuild();
491 })("article_pgup", "artiles list: page up");
493 conRegFunc!(() {
494 if (auto fld = getActiveFolder) {
495 fld.movePageDown();
496 postScreenRebuild();
498 })("article_pgdown", "artiles list: page down");
500 conRegFunc!(() {
501 if (auto fld = getActiveFolder) {
502 fld.scrollUp();
503 postScreenRebuild();
505 })("article_scroll_up", "scroll article list up");
507 conRegFunc!(() {
508 if (auto fld = getActiveFolder) {
509 fld.scrollDown();
510 postScreenRebuild();
512 })("article_scroll_down", "scroll article list up");
514 conRegFunc!(() {
515 if (auto fld = getActiveFolder) {
516 fld.moveToFirst();
517 postScreenRebuild();
519 })("article_to_first", "go to first article");
521 conRegFunc!(() {
522 if (auto fld = getActiveFolder) {
523 fld.moveToLast();
524 postScreenRebuild();
526 })("article_to_last", "go to last article");
529 // //////////////////////////////////////////////////////////////////// //
530 conRegFunc!(() {
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;
542 } else {
543 pw.title = "Reply from '"~acc.name~"' ("~acc.mail~")";
544 pw.to.str = "";
545 pw.activeWidget = pw.to;
547 pw.subj.str = "";
548 pw.acc = acc;
549 pw.fld = fld;
552 auto acc = fld.findAccountToPost();
553 if (acc is null) {
554 auto wacc = new SelectPopBoxWindow(defaultAcc);
555 wacc.onSelected = postDg;
556 } else {
557 postDg(acc);
558 acc = defaultAcc;
560 } else {
561 conwriteln("post: no active folder");
563 })("new_post", "post a new article or message");
566 // //////////////////////////////////////////////////////////////////// //
567 conRegFunc!(() {
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;
585 } else {
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-");
594 if (vp > 0) {
595 from = from[0..vp].xstrip;
596 if (from.length == 0) from = "anonymous";
600 pw.ed.addText(from);
601 pw.ed.addText(" wrote:\n");
602 pw.ed.addText("\n");
603 foreach (ConString s; LineIterator!false(atext)) {
604 if (s.length == 0 || s[0] == '>') pw.ed.addText(">"); else pw.ed.addText("> ");
605 pw.ed.addText(s);
606 pw.ed.addText("\n");
608 pw.ed.addText("\n");
609 pw.ed.reformat();
610 pw.replyto = art.msgid;
611 pw.references = art.getHeaderValue("References").xstrip.idup;
612 pw.acc = acc;
613 pw.fld = fld;
614 pw.activeWidget = pw.ed;
620 auto acc = fld.findAccountToPost(fld.curidx);
621 if (acc is null) {
622 auto wacc = new SelectPopBoxWindow(defaultAcc);
623 wacc.onSelected = postDg;
624 } else {
625 postDg(acc);
626 acc = defaultAcc;
629 })("article_reply", "reply to the current article");
632 // //////////////////////////////////////////////////////////////////// //
633 conRegFunc!(() {
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");
650 conRegFunc!(() {
651 if (auto fld = getActiveFolder) {
652 Article newart;
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();
666 didx = aidx;
671 // delete it
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;
678 art.updated = true;
679 abase.writeUpdates();
681 //abase.remove(didx);
682 //abase.writeUpdates();
684 fld.markForRebuild();
685 fld.buildVisibleList();
687 // insert into spam
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();
699 postScreenRebuild();
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; }
710 Article newart;
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();
719 didx = aidx;
723 if (didx == uint.max || newart is null) { conwriteln("article not found!"); return; }
724 // delete it
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;
731 art.updated = true;
732 abase.writeUpdates();
734 //abase.remove(didx);
735 //abase.writeUpdates();
737 fld.markForRebuild();
738 fld.buildVisibleList();
739 // insert into spam
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();
748 postScreenRebuild();
750 })("article_move_to_folder", "move article to existing folder");
753 // //////////////////////////////////////////////////////////////////// //
754 conRegFunc!(() {
755 if (auto fld = getActiveFolder) {
756 fld.moveToParent();
757 postScreenRebuild();
759 })("article_to_parent", "jump to parent article, if any");
761 conRegFunc!(() {
762 if (auto fld = getActiveFolder) {
763 fld.moveToPrevSib();
764 postScreenRebuild();
766 })("article_to_prev_sib", "jump to previous sibling");
768 conRegFunc!(() {
769 if (auto fld = getActiveFolder) {
770 fld.moveToNextSib();
771 postScreenRebuild();
773 })("article_to_next_sib", "jump to next sibling");
776 // //////////////////////////////////////////////////////////////////// //
777 conRegFunc!(() {
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) {
785 string xcmd;
786 if (t is null) {
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);
790 } else {
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);
793 } else {
794 concmdfex!"twit_set name \"%s\" mail \"%s\" folder_mask \"%s\" title \"%s\""((ConString cmd) { xcmd = cmd.xstrip.idup; }, name, mail, folder, title);
797 try {
798 import std.path : buildPath;
799 auto fo = VFile(buildPath(mailRootDir, "auto_twits.rc"), "a");
800 fo.writeln(xcmd);
801 } catch (Exception e) {
802 conwriteln("ERROR writing twit info: ", e.msg);
805 // HACK: FIXME!
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 // //////////////////////////////////////////////////////////////////// //
817 conRegFunc!(() {
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
829 postScreenRebuild();
831 })("article_twit_thread", "twit current thread");
833 conRegFunc!(() {
834 if (auto fld = getActiveFolder) {
835 if (fld.toggleSoftDeleted()) postScreenRebuild();
837 })("article_softdelete_toggle", "toggle \"soft deleted\" flag on current article");
840 // //////////////////////////////////////////////////////////////////// //
841 conRegFunc!(() {
842 if (auto fld = getActiveFolder) {
843 if (fld.curidxValid) {
844 fld.withBaseReader(delegate (abase, cur, top, alist) {
845 auto art = abase[alist[cur]];
846 if (art !is null) {
847 conwriteln("============================");
848 abase.loadContent(alist[cur]);
849 scope(exit) art.releaseContent();
850 ConString from;
851 foreach (ConString s; art.headersIterator!false) {
852 conwriteln(" ", s);
853 if (from.length == 0 && s.startsWithCI("From:")) from = s;
855 conwriteln("---------------");
856 conwriteln(" ", art.fromname, " <", art.frommail, ">");
857 conwriteln(" ", decodeq(from));
858 conwrite(" ");
859 Utf8DecoderFast dc;
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);
866 conwriteln();
871 })("article_dump_headers", "dump article headers");
873 conRegFunc!(() {
874 if (auto fld = getActiveFolder) {
875 fld.rebuildIndex();
876 postScreenRebuild();
878 })("folder_rebuild_index", "rebuild index file");
880 conRegFunc!(() {
881 if (auto fld = getActiveFolder) {
882 fld.packText();
883 postScreenRebuild();
885 })("folder_pack_textdb", "pack (rebuild) text database for current folder");
887 conRegVar("folder_hide_old", "should old articles be hidden in UI?",
888 delegate (self) {
889 if (auto fld = getActiveFolder) return fld.hideOldThreads;
890 return false;
892 delegate (self, bool nv) {
893 if (auto fld = getActiveFolder) fld.hideOldThreads = nv;
898 conRegFunc!(() {
899 if (auto fld = getActiveFolder) {
900 fld.withBase(delegate (abase) {
901 uint idx = fld.curidx;
902 if (idx >= fld.length) {
903 idx = 0;
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)];
911 if (art !is null) {
912 if (art.frommail.indexOf("ketmar@ketmar.no-ip.org") >= 0) {
913 fld.curidx = cast(int)idx;
914 postScreenRebuild();
915 return;
918 idx = (idx+1)%fld.length;
922 })("find_mine", "find mine article");
925 // //////////////////////////////////////////////////////////////////// //
926 conRegFunc!(() {
927 new FontWindow();
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();
942 try {
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);
947 if (fname.isDir) {
948 string res = fname.idup;
949 if (res.length && res[$-1] != '/') res ~= '/';
950 res ~= Article.fixAttachmentName(attname);
951 return res;
953 return fname.idup;
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;
990 this () {
991 super(null, 0, 0, VBufWidth, VBufHeight);
992 mType = Type.OnBottom;
993 add();
996 // //////////////////////////////////////////////////////////////////// //
997 static struct WebLink {
998 int ly; // in lines
999 int x, len; // in pixels
1000 string url;
1001 string text; // visual text
1002 int attachnum = -1;
1004 @property bool isAttach () const pure nothrow @safe @nogc { return (attachnum >= 0); }
1007 WebLink[] emurls;
1008 int lastUrlIndex = -1;
1010 void clearDecodedText () {
1011 emurls[] = WebLink.init;
1012 emlines[] = null;
1013 emurls.length = 0;
1014 emurls.assumeSafeAppend;
1015 emlines.length = 0;
1016 emlines.assumeSafeAppend;
1017 lastDecodedMsgFolderPath = null;
1018 lastDecodedMsgId = null;
1019 articleTextTopLine = 0;
1020 articleDestTextTopLine = 0;
1021 lastUrlIndex = -1;
1024 // <0: not on url
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;
1036 mx -= tpX0;
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) {
1043 if (wl.ly == idx) {
1044 if (mx >= wl.x && mx < wl.x+wl.len) return cast(int)uidx;
1048 ++idx;
1049 y += gxTextHeightUtf;
1052 return -1;
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" ];
1063 lastUrlIndex = -1;
1065 static sptrdiff urlepos (const(char)[] s, sptrdiff spos) {
1066 assert(spos < s.length);
1067 spos = s.indexOf("://", spos);
1068 assert(spos >= 0);
1069 spos += 3;
1070 // host
1071 while (spos < s.length) {
1072 char ch = s[spos];
1073 if (ch == '/') break;
1074 if (ch <= ' ') return spos;
1075 if (ch != '.' && ch != '-' && !ch.isalnum) return spos;
1076 ++spos;
1078 if (spos >= s.length) return spos;
1079 // path
1080 assert(s[spos] == '/');
1081 while (spos < s.length) {
1082 char ch = s[spos];
1083 if (ch <= ' ' || ch == '<' || ch == '>') return spos;
1084 if (ch == '.' || ch == ')' || ch == '-' || ch == '!' || ch == ';') {
1085 if (s.length-spos > 1) {
1086 ch = s[spos+1];
1087 if (ch <= ' ' || ch == '.' || ch == '(' || ch == ')' || ch == '-' || ch == '!' || ch == ';' || ch == '<' || ch == '>') return spos;
1090 ++spos;
1092 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("://");
1099 while (pos > 0) {
1100 bool found = false;
1101 auto spos = pos;
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) {
1105 found = true;
1106 spos -= proto.length;
1107 break;
1111 if (found) {
1112 // find URL end
1113 auto epos = urlepos(s, spos);
1114 //conwriteln("spos=", spos, "; epos=", epos, "; s=[", s, "]; link=[", s[spos..epos], "]");
1115 WebLink wl;
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;
1121 emurls ~= wl;
1122 pos = epos;
1123 } else {
1124 ++pos;
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;
1137 auto epos = spos+7;
1138 while (epos < s.length && s.ptr[epos] > ' ') ++epos;
1139 //if (attachcount >= parts.length) break;
1140 WebLink wl;
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];
1149 ++attachcount;
1150 emurls ~= wl;
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;
1167 clearDecodedText();
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); }
1174 int lastQLevel = 0;
1176 void putLine (ConString s) {
1177 enum QuoteStr = ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ";
1178 static string addQuotes(T:const(char)[]) (T s, int qlevel) {
1179 if (qlevel <= 0) {
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
1186 int qlevel = 0;
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;
1192 } else {
1193 lastqpos = pos;
1194 ++qlevel;
1196 ++pos;
1198 if (s.length-lastqpos > 1 && s.ptr[lastqpos+1] == ' ') ++lastqpos;
1199 ++lastqpos;
1200 s = s[lastqpos..$];
1202 //conwriteln("qlevel=", qlevel, "; s=[", s, "]");
1203 // empty line: just insert it
1204 if (s.length == 0) {
1205 emlines ~= addQuotes(null, qlevel).xstripright;
1206 } else {
1207 // can we append?
1208 bool newline = false;
1209 if (lastQLevel != qlevel || !lastEndsWithSpace) {
1210 newline = true;
1211 } else {
1212 // append words
1213 //while (s.length > 1 && s.ptr[0] <= ' ') s = s[1..$];
1215 while (s.length) {
1216 usize epos = 0;
1217 if (newline) {
1218 while (epos < s.length && s.ptr[epos] <= ' ') ++epos;
1219 } else {
1220 //assert(s[0] > ' ');
1221 while (epos < s.length && s.ptr[epos] <= ' ') ++epos;
1223 while (epos < s.length && s.ptr[epos] > ' ') ++epos;
1224 auto xlen = 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];
1229 } else {
1230 newline = false;
1231 // wrapping; new line
1232 emlines ~= addQuotes(s[0..epos], qlevel);
1234 s = s[epos..$];
1236 if (newline) emlines ~= addQuotes(null, qlevel);
1238 lastQLevel = qlevel;
1241 try {
1242 foreach (ConString s; LineIterator!false(art.getTextContent)) {
1243 static if (doLocalConvert) {
1244 putLine(s.uniRecode);
1245 } else {
1246 putLine(s);
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);
1259 // attaches
1260 auto lcount = cast(uint)emlines.length;
1261 emlinesBeforeAttaches = lcount;
1263 static if (doLocalConvert) {
1264 uint attcount = 0;
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);
1270 emlines ~= s;
1271 ++attcount;
1272 return false;
1274 emlDetectUrls(lcount);
1275 } else {
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;
1290 } else {
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;
1301 foreach (; 0..6) {
1302 normalizeArticleTopLine();
1303 if (articleDestTextTopLine < articleTextTopLine) {
1304 --articleTextTopLine;
1305 } else if (articleDestTextTopLine > articleTextTopLine) {
1306 ++articleTextTopLine;
1307 } else {
1308 break;
1310 normalizeArticleTopLine();
1312 postScreenRebuild();
1313 if (articleTextTopLine == oldtop) {
1314 // can't scroll anymore
1315 articleDestTextTopLine = articleTextTopLine;
1316 return;
1318 postArticleScroll();
1321 void scrollBy (int delta) {
1322 articleDestTextTopLine += delta;
1323 doScrollStep();
1326 void scrollByPageUp () {
1327 int lines = visibleArticleLines-1;
1328 if (lines < 1) lines = 1;
1329 scrollBy(-lines);
1332 void scrollByPageDown () {
1333 int lines = visibleArticleLines-1;
1334 if (lines < 1) lines = 1;
1335 scrollBy(lines);
1338 // //////////////////////////////////////////////////////////////////// //
1339 //TODO: move parts to widgets
1340 override void onPaint () {
1341 clipReset();
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;
1354 char[128] tbuf;
1355 const(char)[] tbufs;
1357 void xfmt (string s, const(char)[][] strs...) {
1358 int dpos = 0;
1359 void puts (const(char)[] s...) {
1360 foreach (char ch; s) {
1361 if (dpos >= tbuf.length) break;
1362 tbuf[dpos++] = ch;
1365 while (s.length) {
1366 if (strs.length && s.length > 1 && s[0] == '%' && s[1] == 's') {
1367 puts(strs[0]);
1368 strs = strs[1..$];
1369 s = s[2..$];
1370 } else {
1371 puts(s[0]);
1372 s = s[1..$];
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;
1393 // header
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));
1408 // text
1409 int y = clipY0+3*gxTextHeightUtf+2;
1410 immutable sty = y;
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) {
1420 int qlevel = 0;
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;
1427 ++qlevel;
1430 clr = gxRGB!( 0, 128+40, 0);
1431 if (qlevel) {
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) {
1441 if (wl.ly == idx) {
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;
1452 ++idx;
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;
1488 clipY0 = 0;
1489 clipY1 = guiThreadListHeight-1;
1490 immutable uint origX0 = clipX0;
1491 immutable uint origX1 = clipX1;
1492 immutable uint origY0 = clipY0;
1493 immutable uint origY1 = clipY1;
1494 int y = 0;
1495 //conwriteln(fld.msgtop, " : ", fld.list.length);
1496 uint idx = top;
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;
1504 clipX0 = origX0;
1505 clipX1 = origX1;
1507 auto art = abase[alist[idx]];
1509 //conwriteln(idx, " : ", fld.list.length);
1510 if (idx == cur) {
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);
1515 ++clipX0;
1516 --clipX1;
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;
1540 char[128] tmpbuf;
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-");
1549 if (vp > 0) {
1550 from = from[0..vp].xstrip;
1551 if (from.length == 0) from = "anonymous";
1555 clipX1 -= 13*6+4;
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);
1561 clipX1 -= 22*6+4;
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) {
1566 clipX0 = origX0;
1567 clipX1 = origX1;
1568 gxHLine(clipX0, y+gxTextHeightUtf/2, clipX1-clipX0+1, clr);
1571 ++idx;
1572 y += gxTextHeightUtf;
1575 // draw progressbar
1577 //if (idx > fld.list.length) idx = cast(uint)fld.list.length;
1578 clipX0 = origX0;
1579 clipX1 = origX1+4;
1580 clipY0 = origY0;
1581 clipY1 = origY1;
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));
1586 // frame
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();
1599 int ofsx = 2;
1600 int ofsy = 1;
1601 foreach (immutable idx, Folder fld; folders) {
1602 if (idx < folderTop) continue;
1603 if (ofsy >= VBufHeight) break;
1604 clipReset();
1605 if (idx == folderCur) gxFillRect(0, ofsy-1, guiGroupListWidth, (gxTextHeightUtf+2), gxRGB!(0, 127, 127));
1606 clipX0 = ofsx-1;
1607 clipY0 = ofsy;
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);
1613 } else {
1614 if (depth == 0) {
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);
1633 char[64] kname;
1634 if (auto cmdp = event.toStrBuf(kname[]) in mainAppKeyBindings) {
1635 concmd(*cmdp);
1636 return true;
1638 // debug
1640 if (event == "S-Up") {
1641 if (folderTop > 0) --folderTop;
1642 postScreenRebuild();
1643 return true;
1645 if (event == "S-Down") {
1646 if (folderTop+1 < folders.length) ++folderTop;
1647 postScreenRebuild();
1648 return true;
1651 //if (event == "Tab") { new PostWindow(); return true; }
1652 //if (event == "Tab") { new SelectPopBoxWindow(null); return true; }
1654 return false;
1657 // returning `false` to avoid screen rebuilding by dispatcher
1658 override bool onMouse (MouseEvent event) {
1659 //FIXME: use window coordinates
1660 int mx, my;
1661 event.mouse2xy(mx, my);
1662 // button press
1663 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
1664 // select folder
1665 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < VBufHeight) {
1666 uint fnum = my/(gxTextHeightUtf+2)+folderTop;
1667 if (fnum != folderCur) {
1668 folderCur = fnum;
1669 postScreenRebuild();
1671 return false;
1673 // select post
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();
1679 return false;
1683 // wheel
1684 if (event.type == MouseEventType.buttonPressed && (event.button == MouseButton.wheelUp || event.button == MouseButton.wheelDown)) {
1685 // folder
1686 if (mx >= 0 && mx < guiGroupListWidth && my >= 0 && my < VBufHeight) {
1687 if (event.button == MouseButton.wheelUp) {
1688 if (folderCur > 0) folderCur = folderCur-1;
1689 } else {
1690 if (folderCur+1 < folders.length) folderCur = folderCur+1;
1692 postScreenRebuild();
1693 return false;
1695 // post
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();
1700 return false;
1703 // text
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();
1708 return false;
1711 // button release
1712 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
1713 // try url
1714 auto url = findUrlAt(mx, my);
1715 if (url !is null) {
1716 if (url.isAttach) {
1717 concmdf!"attach_save %s"(url.attachnum);
1718 } else {
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();
1730 } else {
1731 postScreenRepaint();
1734 return false;
1737 override bool onChar (dchar ch) {
1738 return false;
1743 // ////////////////////////////////////////////////////////////////////////// //
1744 void repaintScreen () {
1745 clipReset();
1746 gxClearScreen(0);
1747 paintSubWindows();
1748 vglUpdateTexture();
1752 // ////////////////////////////////////////////////////////////////////////// //
1753 class FontWindow : SubWindow {
1754 int cx, cy;
1756 this () {
1757 super("", 16*14+4, 16*14+4+10);
1758 //if (hasWindowClass(this)) return;
1759 add();
1762 override void onPaint () {
1763 import core.stdc.stdio : snprintf;
1764 char[64] buf;
1766 auto tlen = snprintf(buf.ptr, buf.length, "Font: \\x%02x (%d)", cast(int)(cy*16+cx), cast(int)(cy*16+cx));
1768 setupClip();
1769 gxDrawWindow(buf[0..tlen], gxRGB!(255, 255, 255), gxRGB!(0, 0, 0), gxRGB!(255, 255, 255), gxRGB!(0, 0, 180));
1771 setupClientClip();
1772 clipX0 += 2;
1773 clipY0 += 0;
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";
1818 initConsole();
1819 hitwitInitConsole();
1821 clearBindings();
1822 setupDefaultBindings();
1824 concmd("exec chiroptera.rc tan");
1826 scanFolders();
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);
1848 vbwin.hideCursor();
1850 vbwin.onFocusChange = delegate (bool focused) {
1851 vbfocused = focused;
1852 if (!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!");
1877 vglTexBuf = nv;
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;
1886 lastWinWidth = wdt;
1887 lastWinHeight = hgt;
1889 // reinitialize OpenGL texture
1891 import iv.glbinds;
1893 enum wrapOpt = GL_REPEAT;
1894 enum filterOpt = GL_NEAREST; //GL_LINEAR;
1895 enum ttype = GL_UNSIGNED_BYTE;
1897 if (vglTexId) glDeleteTextures(1, &vglTexId);
1898 vglTexId = 0;
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);
1920 mouseMoved();
1922 repaintScreen();
1923 vbwin.redrawOpenGlSceneNow();
1926 vbwin.addEventListener((DoConsoleCommandsEvent evt) {
1927 bool sendAnother = false;
1928 bool prevVisible = isConsoleVisible;
1930 consoleLock();
1931 scope(exit) consoleUnlock();
1932 conProcessQueue();
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;
1946 repaintScreen();
1947 vbwin.redrawOpenGlSceneNow();
1951 vbwin.addEventListener((ScreenRebuildEvent evt) {
1952 if (vbwin.closed) return;
1953 if (isQuitRequested) { vbwin.close(); return; }
1954 repaintScreen();
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;
1968 repaintScreen();
1969 vbwin.redrawOpenGlSceneNow();
1972 vbwin.addEventListener((QuitEvent evt) {
1973 if (vbwin.closed) return;
1974 if (isQuitRequested) { vbwin.close(); return; }
1975 vbwin.close();
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;
2009 } else {
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) {
2025 if (uphintWindow) {
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
2031 repaintScreen();
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;
2055 consoleLock();
2056 scope(exit) consoleUnlock();
2058 if (!conQueueEmpty()) postDoConCommands();
2060 if (VBufScale != vbufEffScale) {
2061 // window scale changed
2062 vbufEffScale = VBufScale;
2063 resizeWin = true;
2065 if (vbufEffVSync != vbufVSync) {
2066 vbufEffVSync = vbufVSync;
2067 vbwin.vsync = vbufEffVSync;
2071 if (resizeWin) {
2072 vbwin.resize(winWidthScaled, winHeightScaled);
2073 glconResize(winWidthScaled, winHeightScaled);
2074 rebuildTexture = true;
2077 if (rebuildTexture) repaintScreen();
2079 glMatrixMode(GL_PROJECTION); // for ortho camera
2080 glLoadIdentity();
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);
2087 glLoadIdentity();
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);
2100 if (vglTexId) {
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);
2107 glBegin(GL_QUADS);
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
2112 glEnd();
2115 if (vArrowTextureId) {
2116 if (isMouseVisible) {
2117 int px = lastMouseX;
2118 int py = lastMouseY;
2119 glEnable(GL_BLEND);
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);
2125 glBegin(GL_QUADS);
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
2130 glEnd();
2134 glconDraw();
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 () {
2144 import iv.glbinds;
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);
2180 repaintScreen();
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);
2189 try {
2190 foreach (immutable idx; 0..6) {
2191 string fname = "databinz/icons.zip:icons";
2192 if (idx == 0) {
2193 fname ~= "/main.png";
2194 } else {
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();
2226 repostHideMouse();
2228 vbwin.eventLoop(1000*10,
2229 delegate () {
2230 scope(exit) if (!conQueueEmpty()) postDoConCommands();
2231 if (vbwin.closed) return;
2233 consoleLock();
2234 scope(exit) consoleUnlock();
2235 conProcessQueue();
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();
2245 return;
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;
2258 mouseMoved();
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();
2266 return;
2268 if (dispatchEvent(ch)) return;
2271 saveCurrentFolderAndPosition();
2272 trayimages[] = null;
2273 if (trayicon !is null && !trayicon.closed) { trayicon.close(); trayicon = null; }
2274 flushGui();
2275 updateThreadId.send(UpThreadCommand.Quit);
2276 conProcessQueue(int.max/4);