fixes for new NanoVega API
[xreader.git] / xreader.d
blob2b7a3ac65accf410373d576c98d62cef42b6a06f
1 /* Written by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>f
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module xreader is aliced;
19 import core.time;
21 import std.concurrency;
22 import std.net.curl;
23 import std.regex;
25 import arsd.simpledisplay;
26 import arsd.image;
28 import iv.nanovega;
29 import iv.nanovega.textlayouter;
30 import iv.nanovega.blendish;
31 import iv.nanovega.perf;
33 import iv.sdbm;
34 import iv.strex;
36 import iv.cmdcongl;
38 import iv.vfs;
39 import iv.vfs.io;
41 import xmodel;
42 import xiniz;
44 import booktext;
46 import xreadercfg;
47 import xreaderui;
48 import xreaderifs;
49 import xreaderfmt;
51 import eworld;
52 import ewbillboard;
54 //version = debug_draw;
57 // ////////////////////////////////////////////////////////////////////////// //
58 __gshared int oglConScale = 1;
61 // ////////////////////////////////////////////////////////////////////////// //
62 __gshared string bookFileName;
64 __gshared int formatWorks = -1;
65 __gshared NVGContext vg = null;
66 __gshared PerfGraph fps;
67 __gshared bool fpsVisible = false;
68 __gshared BookText bookText;
69 __gshared string newBookFileName;
70 __gshared uint[] posstack;
71 __gshared SimpleWindow sdwindow;
72 __gshared LayTextC laytext;
73 __gshared BookInfo[] recentFiles;
74 __gshared BookMetadata bookmeta;
76 __gshared int textHeight;
78 __gshared int toMove = 0; // for smooth scroller
79 __gshared int topY = 0;
80 __gshared int arrowDir = 0;
81 __gshared int arrowSpeed = 0;
83 __gshared int newYLine = -1; // index
84 __gshared float newYAlpha;
85 __gshared bool newYFade = false;
86 __gshared MonoTime nextFadeTime;
88 __gshared NVGImage curImg, curImgWhite;
89 __gshared int mouseX = -666, mouseY = -666;
90 __gshared bool mouseHigh = false;
91 __gshared bool mouseHidden = false;
92 __gshared MonoTime lastMMove;
93 __gshared float mouseAlpha = 1.0f;
94 __gshared int mouseFadingAway = false;
96 __gshared bool needRedrawFlag = true;
98 __gshared bool firstFormat = true;
100 __gshared bool inGalaxyMap = false;
101 __gshared PopupMenu currPopup;
102 __gshared void delegate (int item) onPopupSelect;
103 __gshared bool delegate (KeyEvent event) onPopupKeyEvent; // return `true` to close menu
104 __gshared int shipModelIndex = 0;
105 __gshared bool popupNoShipKill = false;
107 __gshared bool doSaveCheck = false;
108 __gshared MonoTime nextSaveTime;
111 // ////////////////////////////////////////////////////////////////////////// //
112 void refresh () { needRedrawFlag = true; }
113 void refreshed () { needRedrawFlag = false; }
115 bool needRedraw () {
116 return (needRedrawFlag || (currPopup !is null && currPopup.needRedraw));
120 @property bool inMenu () { return (currPopup !is null); }
121 void closeMenu () { if (currPopup !is null) currPopup.destroy; currPopup = null; onPopupSelect = null; onPopupKeyEvent = null; }
124 void setShip (int idx) {
125 if (eliteShipFiles.length) {
126 import core.memory : GC;
127 if (idx >= eliteShipFiles.length) idx = cast(int)eliteShipFiles.length-1;
128 if (idx < 0) idx = 0;
129 if (shipModelIndex == idx && shipModel !is null) return;
130 // remove old ship
131 shipModelIndex = idx;
132 if (shipModel !is null) {
133 shipModel.glUnload();
134 shipModel.freeData();
135 shipModel.destroy;
136 shipModel = null;
138 GC.collect();
139 // load new ship
140 try {
141 shipModel = new EliteModel(eliteShipFiles[shipModelIndex]);
142 shipModel.glUpload();
143 shipModel.freeImages();
144 } catch (Exception e) {}
145 //shipModel = eliteShips[shipModelIndex];
146 GC.collect();
150 void ensureShipModel () {
151 if (eliteShipFiles.length && shipModel is null) {
152 import std.random : uniform;
153 setShip(uniform!"[)"(0, eliteShipFiles.length));
157 void freeShipModel () {
158 if (!showShip && !inMenu && shipModel !is null) {
159 import core.memory : GC;
160 shipModel.glUnload();
161 shipModel.freeData();
162 shipModel.destroy;
163 shipModel = null;
164 GC.collect();
169 void loadState () {
170 try {
171 int widx = -1;
172 xiniParse(VFile(stateFileName),
173 "wordindex", &widx,
175 if (widx >= 0) goTo(widx);
176 } catch (Exception) {}
179 void doSaveState (bool forced=false) {
180 if (!forced) {
181 if (formatWorks != 0) return;
182 if (!doSaveCheck) return;
183 auto ct = MonoTime.currTime;
184 if (ct < nextSaveTime) return;
185 } else {
186 if (laytext is null || laytext.lineCount == 0 || formatWorks != 0) return;
188 try {
189 auto fo = VFile(stateFileName, "w");
190 if (laytext !is null && laytext.lineCount) {
191 auto lnum = laytext.findLineAtY(topY);
192 if (lnum >= 0) fo.writeln("wordindex=", laytext.line(lnum).wstart);
194 } catch (Exception) {}
195 doSaveCheck = false;
198 void stateChanged () {
199 if (!doSaveCheck) {
200 doSaveCheck = true;
201 nextSaveTime = MonoTime.currTime+10.seconds;
206 void hardScrollBy (int delta) {
207 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
208 int oldY = topY;
209 topY += delta;
210 if (topY >= laytext.textHeight-textHeight) topY = cast(int)laytext.textHeight-textHeight;
211 if (topY < 0) topY = 0;
212 if (topY != oldY) {
213 stateChanged();
214 refresh();
218 void scrollBy (int delta) {
219 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
220 toMove += delta;
221 newYFade = true;
222 newYAlpha = 1;
223 if (delta < 0) {
224 // scrolling up, mark top line
225 newYLine = laytext.findLineAtY(topY);
226 } else {
227 // scrolling down, mark bottom line
228 newYLine = laytext.findLineAtY(topY+textHeight-2);
230 version(none) {
231 conwriteln("scrollBy: delta=", delta, "; newYLine=", newYLine, "; topLine=", laytext.findLineAtY(topY));
233 if (newYLine < 0) newYFade = false;
237 void goHome () {
238 if (laytext is null) return;
239 if (topY != 0) {
240 topY = 0;
241 stateChanged();
242 refresh();
247 void goEnd () {
248 if (laytext is null || laytext.lineCount < 2) return;
249 auto newY = laytext.line(laytext.lineCount-1).y;
250 if (newY >= laytext.textHeight-textHeight) newY = cast(int)laytext.textHeight-textHeight;
251 if (newY < 0) newY = 0;
252 if (newY != topY) {
253 topY = newY;
254 stateChanged();
255 refresh();
260 void goTo (uint widx) {
261 if (laytext is null) return;
262 auto lidx = laytext.findLineWithWord(widx);
263 if (lidx != -1) {
264 assert(lidx < laytext.lineCount);
265 toMove = 0;
266 if (topY != laytext.line(lidx).y) {
267 topY = laytext.line(lidx).y;
268 stateChanged();
269 refresh();
271 version(none) {
272 conwriteln("goto: widx=", widx, "; lidx=", lidx, "; fwn=", laytext.line(lidx).wstart, "; topY=", topY);
273 auto lnum = laytext.findLineAtY(topY);
274 conwriteln(" newlnum=", lnum, "; lidx.y=", laytext.line(lidx).y, "; lnum.y=", laytext.line(lnum).y);
280 void pushPosition () {
281 if (laytext is null || laytext.lineCount == 0) return;
282 auto lidx = laytext.findLineAtY(topY);
283 if (lidx >= 0) posstack ~= laytext.line(lidx).wstart;
286 void popPosition () {
287 if (posstack.length == 0) return;
288 auto widx = posstack[$-1];
289 posstack.length -= 1;
290 posstack.assumeSafeAppend;
291 goTo(widx);
295 void gotoSection (int sn) {
296 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) return;
297 if (sn < 0 || sn >= bookmeta.sections.length) return;
298 auto lidx = laytext.findLineWithWord(bookmeta.sections[sn].wordidx);
299 if (lidx >= 0) {
300 auto newY = laytext.line(lidx).y;
301 if (newY != topY) {
302 topY = newY;
303 stateChanged();
304 refresh();
310 void relayout (bool forced=false) {
311 if (laytext !is null) {
312 uint widx;
313 auto lidx = laytext.findLineAtY(topY);
314 if (lidx >= 0) widx = laytext.line(lidx).wstart;
315 int maxWidth = GWidth-4-2-BND_SCROLLBAR_WIDTH-2;
316 //if (maxWidth < MinWinWidth) maxWidth = MinWinWidth;
318 import core.time;
319 auto stt = MonoTime.currTime;
320 laytext.relayout(maxWidth, forced);
321 auto ett = MonoTime.currTime-stt;
322 conwriteln("relayouted in ", ett.total!"msecs", " milliseconds; lines:", laytext.lineCount, "; words:", laytext.nextWordIndex);
324 goTo(widx);
325 refresh();
330 void drawShipName () {
331 if (shipModel is null || shipModel.name.length == 0) return;
332 vg.fontFaceId(uiFont);
333 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
334 vg.fontSize(fsizeUI);
335 auto w = vg.bndLabelWidth(-1, shipModel.name)+8;
336 float h = BND_WIDGET_HEIGHT+8;
337 float mx = (GWidth-w)/2.0;
338 float my = (GHeight-h)-8;
339 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
340 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.name);
341 if (shipModel.dispName && shipModel.dispName != shipModel.name) {
342 my -= BND_WIDGET_HEIGHT+16;
343 w = vg.bndLabelWidth(-1, shipModel.dispName)+8;
344 mx = (GWidth-w)/2.0;
345 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
346 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.dispName);
351 void createSectionMenu () {
352 closeMenu();
353 //conwriteln("lc=", laytext.lineCount, "; bm: ", (bookmeta !is null));
354 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) { freeShipModel(); return; }
355 currPopup = new PopupMenu(vg, "Sections"d, () {
356 dstring[] items;
357 foreach (const ref sc; bookmeta.sections) items ~= sc.name;
358 return items;
360 currPopup.allowFiltering = true;
361 //conwriteln(currPopup.items.length);
362 // find current section
363 currPopup.itemIndex = 0;
364 auto lidx = laytext.findLineAtY(topY);
365 if (lidx >= 0 && bookmeta.sections.length > 0) {
366 foreach (immutable sidx, const ref sc; bookmeta.sections) {
367 auto sline = laytext.findLineWithWord(sc.wordidx);
368 if (sline >= 0 && lidx >= sline) currPopup.itemIndex = cast(int)sidx;
371 onPopupSelect = (int item) { gotoSection(item); };
375 void createQuitMenu (bool wantYes) {
376 closeMenu();
377 currPopup = new PopupMenu(vg, "Quit?"d, () {
378 return ["Yes"d, "No"d];
380 currPopup.itemIndex = (wantYes ? 0 : 1);
381 onPopupSelect = (int item) { if (item == 0) concmd("quit"); };
385 void createRecentMenu () {
386 closeMenu();
387 if (recentFiles.length == 0) recentFiles = loadDetailedHistory();
388 if (recentFiles.length == 0) { freeShipModel(); return; }
389 currPopup = new PopupMenu(vg, "Recent files"d, () {
390 import std.conv : to;
391 dstring[] res;
392 foreach (const ref BookInfo bi; recentFiles) {
393 string s = bi.title;
394 if (bi.seqname.length) {
395 s ~= " (";
396 if (bi.seqnum) { s ~= to!string(bi.seqnum); s ~= ": "; }
397 //conwriteln(bi.seqname);
398 s ~= bi.seqname;
399 s ~= ")";
401 if (bi.author.length) { s ~= " \xe2\x80\x94 "; s ~= bi.author; }
402 res ~= s.to!dstring;
404 return res;
406 currPopup.allowFiltering = true;
407 currPopup.itemIndex = cast(int)recentFiles.length-1;
408 onPopupSelect = (int item) {
409 newBookFileName = recentFiles[item].diskfile;
410 popupNoShipKill = true;
412 onPopupKeyEvent = (KeyEvent event) {
413 if (event == "Delete" && currPopup.isCurValid) {
414 auto idx = currPopup.itemIndex;
415 if (idx >= 0 && idx < recentFiles.length) {
416 removeFileFromHistory(recentFiles[idx].diskfile);
417 currPopup.removeItem(idx);
418 foreach (immutable c; idx+1..recentFiles.length) recentFiles[c-1] = recentFiles[c];
419 recentFiles.length -= 1;
420 recentFiles.assumeSafeAppend;
423 return false; // don't close menu
428 bool menuKey (KeyEvent event) {
429 if (formatWorks != 0) return false;
430 if (!inMenu) return false;
431 if (inGalaxyMap) return false;
432 if (!event.pressed) return false;
433 if (currPopup is null) return false;
434 auto res = currPopup.onKey(event);
435 if (res == PopupMenu.Close) {
436 closeMenu();
437 freeShipModel();
438 refresh();
439 } else if (res >= 0) {
440 if (onPopupSelect !is null) onPopupSelect(res);
441 closeMenu();
442 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
443 refresh();
444 } else if (res == PopupMenu.NotMine) {
445 if (onPopupKeyEvent !is null && onPopupKeyEvent(event)) {
446 closeMenu();
447 freeShipModel();
448 refresh();
451 return true;
455 bool menuChar (dchar dch) {
456 if (formatWorks != 0) return false;
457 if (!inMenu) return false;
458 if (inGalaxyMap) return false;
459 if (currPopup is null) return false;
460 auto res = currPopup.onChar(dch);
461 if (res == PopupMenu.Close) {
462 closeMenu();
463 freeShipModel();
464 refresh();
465 } else if (res >= 0) {
466 if (onPopupSelect !is null) onPopupSelect(res);
467 closeMenu();
468 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
469 refresh();
471 return true;
475 bool menuMouse (MouseEvent event) {
476 if (formatWorks != 0) return false;
477 if (!inMenu) return false;
478 if (inGalaxyMap) return false;
479 if (currPopup is null) return false;
480 auto res = currPopup.onMouse(event);
481 if (res == PopupMenu.Close) {
482 closeMenu();
483 freeShipModel();
484 refresh();
485 } else if (res >= 0) {
486 if (onPopupSelect !is null) onPopupSelect(res);
487 closeMenu();
488 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
489 refresh();
491 return true;
495 bool readerKey (KeyEvent event) {
496 if (formatWorks != 0) return false;
497 if (!event.pressed) {
498 switch (event.key) {
499 case Key.Up: arrowDir = 0; return true;
500 case Key.Down: arrowDir = 0; return true;
501 default:
503 return false;
505 switch (event.key) {
506 case Key.Space:
507 if (event.modifierState&ModifierState.shift) {
508 //goto case Key.PageUp;
509 hardScrollBy(toMove); toMove = 0;
510 scrollBy(-textHeight/3*2);
511 } else {
512 //goto case Key.PageDown;
513 hardScrollBy(toMove); toMove = 0;
514 scrollBy(textHeight/3*2);
516 break;
517 case Key.Backspace:
518 popPosition();
519 break;
520 case Key.PageUp:
521 hardScrollBy(toMove); toMove = 0;
522 hardScrollBy(-(textHeight > 32 ? textHeight-32 : textHeight));
523 break;
524 case Key.PageDown:
525 hardScrollBy(toMove); toMove = 0;
526 hardScrollBy(textHeight > 32 ? textHeight-32 : textHeight);
527 break;
528 case Key.Up:
529 //scrollBy(-8);
530 arrowDir = -1;
531 break;
532 case Key.Down:
533 //scrollBy(8);
534 arrowDir = 1;
535 break;
536 case Key.H:
537 if (laytext !is null) {
538 if (event == "C-H") goEnd(); else goHome();
540 break;
541 default:
543 return true;
547 bool controlKey (KeyEvent event) {
548 if (!event.pressed) return false;
549 switch (event.key) {
550 case Key.Escape:
551 if (inGalaxyMap) { inGalaxyMap = false; refresh(); return true; }
552 if (inMenu) { closeMenu(); freeShipModel(); refresh(); return true; }
553 if (showShip) { showShip = false; freeShipModel(); refresh(); return true; }
554 ensureShipModel();
555 createQuitMenu(true);
556 refresh();
557 return true;
558 case Key.P: if (event.modifierState == ModifierState.ctrl) { concmd("r_fps toggle"); return true; } break;
559 case Key.I: if (event.modifierState == ModifierState.ctrl) { concmd("r_interference toggle"); return true; } break;
560 case Key.N: if (event.modifierState == ModifierState.ctrl) { concmd("r_sbleft toggle"); return true; } break;
561 case Key.C: if (event.modifierState == ModifierState.ctrl) { if (addIf()) refresh(); return true; } break;
562 case Key.E:
563 if (event.modifierState == ModifierState.ctrl && eliteShipFiles.length > 0) {
564 if (!inMenu) {
565 showShip = !showShip;
566 if (showShip) ensureShipModel(); else freeShipModel();
567 refresh();
569 return true;
571 break;
572 case Key.Q: if (event.modifierState == ModifierState.ctrl) { concmd("quit"); return true; } break;
573 case Key.B: if (formatWorks == 0 && !inMenu && !inGalaxyMap && event.modifierState == ModifierState.ctrl) { pushPosition(); return true; } break;
574 case Key.S:
575 if (formatWorks == 0 && !showShip && !inMenu && !inGalaxyMap) {
576 ensureShipModel();
577 createSectionMenu();
578 refresh();
580 break;
581 case Key.L:
582 if (formatWorks == 0 && event.modifierState == ModifierState.alt) {
583 ensureShipModel();
584 createRecentMenu();
585 refresh();
587 break;
588 case Key.M:
589 if (!inMenu && !showShip) {
590 inGalaxyMap = !inGalaxyMap;
591 refresh();
593 break;
594 case Key.R:
595 if (formatWorks == 0 && event.modifierState == ModifierState.ctrl) relayout(true);
596 break;
597 case Key.Home: if (showShip) { setShip(0); return true; } break;
598 case Key.End: if (showShip) { setShip(cast(int)eliteShipFiles.length); return true; } break;
599 case Key.Up: case Key.Left: if (showShip) { setShip(shipModelIndex-1); return true; } break;
600 case Key.Down: case Key.Right: if (showShip) { setShip(shipModelIndex+1); return true; } break;
601 default:
603 return showShip;
606 int startX () { pragma(inline, true); return (sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4); }
607 int endX () { pragma(inline, true); return startX+GWidth-4-2-BND_SCROLLBAR_WIDTH-2-1; }
609 int startY () { pragma(inline, true); return (GHeight-textHeight)/2; }
610 int endY () { pragma(inline, true); return startY+textHeight-1; }
613 // ////////////////////////////////////////////////////////////////////////// //
614 void run () {
615 if (GWidth < MinWinWidth) GWidth = MinWinWidth;
616 if (GHeight < MinWinHeight) GHeight = MinWinHeight;
618 bookText = loadBook(bookFileName);
620 //setOpenGLContextVersion(3, 2); // up to GLSL 150
621 setOpenGLContextVersion(2, 0); // it's enough
622 //openGLContextCompatible = false;
624 sdwindow = new SimpleWindow(GWidth, GHeight, bookText.title~" \xe2\x80\x94 "~bookText.authorFirst~" "~bookText.authorLast, OpenGlOptions.yes, Resizability.allowResizing);
625 sdwindow.hideCursor(); // we will do our own
626 sdwindow.setMinSize(MinWinWidth, MinWinHeight);
628 version(X11) sdwindow.closeQuery = delegate () { concmd("quit"); };
630 auto stt = MonoTime.currTime;
631 auto prevt = MonoTime.currTime;
632 auto curt = prevt;
633 textHeight = GHeight-8;
635 MonoTime nextIfTime = MonoTime.currTime;
637 lastMMove = MonoTime.currTime;
639 auto childTid = spawn(&reformatThreadFn, thisTid);
640 childTid.setMaxMailboxSize(128, OnCrowding.block);
641 thisTid.setMaxMailboxSize(128, OnCrowding.block);
643 void loadAndFormat (string filename) {
644 assert(formatWorks <= 0);
645 bookText = null;
646 laytext = null;
647 newYLine = -1;
648 //formatWorks = -1; //FIXME
649 firstFormat = true;
650 newYFade = false;
651 toMove = 0;
652 recentFiles = null;
653 arrowDir = 0;
654 arrowSpeed = 0;
655 //sdwindow.redrawOpenGlSceneNow();
656 //bookText = loadBook(newBookFileName);
657 //newBookFileName = null;
658 //reformat();
659 //if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
660 formatWorks = 1;
661 //conwriteln("*** loading new book: '", filename, "'");
662 childTid.send(ReformatWork(null, filename, GWidth, GHeight));
663 refresh();
666 void reformat () {
667 if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
668 childTid.send(ReformatWork(cast(shared)bookText, null, GWidth, GHeight));
669 refresh();
672 void formatComplete (ref ReformatWorkComplete w) {
673 scope(exit) { import core.memory : GC; GC.collect(); GC.minimize(); }
675 auto lt = cast(LayTextC)w.laytext;
676 scope(exit) if (lt) lt.freeMemory();
677 w.laytext = null;
679 BookMetadata meta = cast(BookMetadata)w.meta;
680 w.meta = null;
682 BookText bt = cast(BookText)w.booktext;
683 w.booktext = null;
684 if (bt !is bookText) {
685 bookText = bt;
686 bookmeta = meta;
687 firstFormat = true;
688 sdwindow.title = bookText.title~" \xe2\x80\x94 "~bookText.authorFirst~" "~bookText.authorLast;
689 } else if (bookmeta is null) {
690 bookmeta = meta;
693 if (isQuitRequested || formatWorks <= 0) return;
694 --formatWorks;
696 if (w.w != GWidth) {
697 if (formatWorks == 0) reformat();
698 return;
701 if (formatWorks != 0) return;
702 freeShipModel();
704 uint widx = 0;
705 if (!firstFormat && laytext !is null && laytext.lineCount) {
706 auto lidx = laytext.findLineAtY(topY);
707 if (lidx >= 0) widx = laytext.line(lidx).wstart;
709 if (laytext !is null) laytext.freeMemory();
710 laytext = lt;
711 lt = null;
712 if (firstFormat) {
713 loadState();
714 firstFormat = false;
715 doSaveCheck = false;
716 stateChanged();
717 } else {
718 goTo(widx);
720 refresh();
723 void closeWindow () {
724 doSaveState(true); // forced state save
725 if (!sdwindow.closed && vg !is null) {
726 curImg.clear();
727 curImgWhite.clear();
728 freeShipModel();
729 vg.kill();
730 vg = null;
731 sdwindow.close();
735 sdwindow.visibleForTheFirstTime = delegate () {
736 sdwindow.setAsCurrentOpenGlContext(); // make this window active
737 sdwindow.vsync = false;
738 //sdwindow.useGLFinish = false;
739 //glbindLoadFunctions();
741 try {
742 NVGContextFlag[4] flagList;
743 uint flagCount = 0;
744 if (flagNanoAA) flagList[flagCount++] = NVGContextFlag.Antialias;
745 if (flagNanoSS) flagList[flagCount++] = NVGContextFlag.StencilStrokes;
746 if (flagNanoFAA) flagList[flagCount++] = NVGContextFlag.FontAA; else flagList[flagCount++] = NVGContextFlag.FontNoAA;
747 vg = nvgCreateContext(flagList[0..flagCount]);
748 if (vg is null) {
749 conwriteln("Could not init nanovg.");
750 assert(0);
751 //sdwindow.close();
753 loadFonts(vg);
754 curImg = createCursorImage(vg);
755 curImgWhite = createCursorImage(vg, true);
756 fps = new PerfGraph("Frame Time", PerfGraph.Style.FPS, "ui");
757 } catch (Exception e) {
758 conwriteln("ERROR: ", e.msg);
759 concmd("quit");
760 return;
763 reformat();
765 refresh();
766 sdwindow.redrawOpenGlScene();
767 refresh();
770 sdwindow.windowResized = delegate (int w, int h) {
771 //conwriteln("w=", w, "; h=", h);
772 //if (w < MinWinWidth) w = MinWinWidth;
773 //if (h < MinWinHeight) h = MinWinHeight;
774 glViewport(0, 0, w, h);
775 GWidth = w;
776 GHeight = h;
777 textHeight = GHeight-8;
778 //reformat();
779 relayout();
780 refresh();
783 sdwindow.redrawOpenGlScene = delegate () {
784 if (isQuitRequested) return;
786 glconResize(GWidth/oglConScale, GHeight/oglConScale, oglConScale);
788 __gshared int cnt;
789 conwriteln("cnt=", cnt++);
792 //glClearColor(0, 0, 0, 0);
793 //glClearColor(0.18, 0.18, 0.18, 0);
794 glClearColor(colorBack.r, colorBack.g, colorBack.b, 0);
795 glClear(glNVGClearFlags|EliteModel.glClearFlags);
797 refreshed();
798 needRedrawFlag = (fps !is null && fpsVisible);
799 if (vg is null) return;
801 scope(exit) vg.releaseImages();
802 vg.beginFrame(GWidth, GHeight, 1);
803 drawIfs(vg);
804 // draw scrollbar
806 float curHeight = (laytext !is null ? topY : 0);
807 float th = (laytext !is null ? laytext.textHeight-textHeight : 0);
808 if (th <= 0) { curHeight = 0; th = 1; }
809 float sz = cast(float)(GHeight-4)/th;
810 if (sz > 1) sz = 1; else if (sz < 0.05) sz = 0.05;
811 int sx = (sbLeft ? 2 : GWidth-BND_SCROLLBAR_WIDTH-2);
812 vg.bndScrollSlider(sx, 2, BND_SCROLLBAR_WIDTH, GHeight-4, BND_DEFAULT, curHeight/th, sz);
814 if (laytext is null) {
815 if (shipModel is null) {
816 vg.beginPath();
817 vg.fillColor(colorText);
818 int drawY = (GHeight-textHeight)/2;
819 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
820 vg.fontFaceId(textFont);
821 vg.fontSize(fsizeText);
822 vg.textAlign(NVGTextAlign.H.Center, NVGTextAlign.V.Middle);
823 vg.text(GWidth/2, GHeight/2, "REFORMATTING");
824 vg.fill();
827 // draw text page
828 int markerY = -666;
829 if (laytext !is null && laytext.lineCount) {
830 vg.beginPath();
831 vg.fillColor(colorText);
832 int drawY = startY;
833 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
834 //FIXME: not GHeight!
835 int lidx = laytext.findLineAtY(topY);
836 if (lidx >= 0 && lidx < laytext.lineCount) {
837 drawY -= topY-laytext.line(lidx).y;
838 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
839 LayFontStyle lastStyle;
840 int startx = startX;
841 bool setColor = true;
842 while (lidx < laytext.lineCount && drawY < GHeight) {
843 auto ln = laytext.line(lidx);
844 foreach (ref LayWord w; laytext.lineWords(lidx)) {
845 if (lastStyle != w.style || setColor) {
846 if (w.style.fontface != lastStyle.fontface) vg.fontFace(laytext.fontFace(w.style.fontface));
847 vg.fontSize(w.style.fontsize);
848 auto c = NVGColor(w.style.color);
849 vg.fillColor(c);
850 //vg.strokeColor(c);
851 lastStyle = w.style;
852 setColor = false;
854 // line highlighting
855 if (newYFade && newYLine == lidx) vg.fillColor(nvgLerpRGBA(colorText, colorTextHi, newYAlpha));
856 auto oid = w.objectIdx;
857 if (oid >= 0) {
858 vg.fill();
859 laytext.objectAtIndex(oid).draw(vg, startx+w.x, drawY+ln.h+ln.desc);
860 vg.beginPath();
861 } else {
862 vg.text(startx+w.x, drawY+ln.h+ln.desc, laytext.wordText(w));
864 //TODO: draw lines over whitespace
865 if (lastStyle.underline) vg.rect(startx+w.x, drawY+ln.h+ln.desc+1, w.w, 1);
866 if (lastStyle.strike) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc/3, w.w, 2);
867 if (lastStyle.overline) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc-1, w.w, 1);
868 version(debug_draw) {
869 vg.fill();
870 vg.beginPath();
871 vg.strokeWidth(1);
872 vg.strokeColor(nvgRGB(0, 0, 255));
873 vg.rect(startx+w.x, drawY, w.w, w.h);
874 vg.stroke();
875 vg.beginPath();
877 if (newYFade && newYLine == lidx) vg.fillColor(NVGColor(lastStyle.color));
879 if (newYFade && newYLine == lidx) markerY = drawY;
880 drawY += ln.h;
881 ++lidx;
884 vg.fill();
886 // draw scroll marker
887 if (markerY != -666) {
888 vg.beginPath();
889 vg.fillColor(NVGColor(0.3f, 0.3f, 0.3f, newYAlpha));
890 vg.rect(startX, markerY, GWidth, 2);
891 vg.fill();
893 // dim text
894 if (!showShip) {
895 if (colorDim.a != 1) {
896 //vg.scissor(0, 0, GWidth, GHeight);
897 vg.resetScissor;
898 vg.beginPath();
899 vg.fillColor(colorDim);
900 vg.rect(0, 0, GWidth, GHeight);
901 vg.fill();
904 // dim more if menu is active
905 if (inMenu || showShip || formatWorks != 0) {
906 //vg.scissor(0, 0, GWidth, GHeight);
907 vg.resetScissor;
908 vg.beginPath();
909 //vg.globalAlpha(0.5);
910 vg.fillColor(nvgRGBA(0, 0, 0, (showShip || formatWorks != 0 ? 127 : 64)));
911 vg.rect(0, 0, GWidth, GHeight);
912 vg.fill();
914 if (shipModel !is null) {
915 vg.endFrame();
916 float zz = shipModel.bbox[1].z-shipModel.bbox[0].z;
917 zz += 10;
918 lightsClear();
920 lightAdd(
921 0, 60, 60,
922 1.0, 0.0, 0.0
925 lightAdd(
926 //0, 0, -60,
927 0, 0, -zz,
928 1.0, 1.0, 1.0
930 drawModel(shipAngle, shipModel);
931 vg.beginFrame(GWidth, GHeight, 1);
932 drawShipName();
933 //vg.endFrame();
935 if (formatWorks == 0) {
936 if (inMenu) {
937 //vg.beginFrame(GWidth, GHeight, 1);
938 //vg.scissor(0, 0, GWidth, GHeight);
939 vg.resetScissor;
940 currPopup.draw();
941 //vg.endFrame();
944 if (fps !is null && fpsVisible) {
945 //vg.beginFrame(GWidth, GHeight, 1);
946 //vg.scissor(0, 0, GWidth, GHeight);
947 vg.resetScissor;
948 fps.render(vg, GWidth-fps.width-4, GHeight-fps.height-4);
949 //vg.endFrame();
951 if (inGalaxyMap) drawGalaxy(vg);
952 // mouse cursor
953 if (curImg.valid && !mouseHidden) {
954 int w, h;
955 //vg.beginFrame(GWidth, GHeight, 1);
956 vg.beginPath();
957 //vg.scissor(0, 0, GWidth, GHeight);
958 vg.resetScissor;
959 vg.imageSize(curImg, w, h);
960 if (mouseFadingAway) {
961 mouseAlpha -= 0.1;
962 if (mouseAlpha <= 0) { mouseFadingAway = false; mouseHidden = true; }
963 } else {
964 mouseAlpha = 1.0f;
966 vg.globalAlpha(mouseAlpha);
967 if (!mouseHigh) {
968 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImg, 1));
969 } else {
970 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 1));
972 vg.rect(mouseX, mouseY, w, h);
973 vg.fill();
975 if (mouseHigh) {
976 vg.beginPath();
977 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 0.4));
978 vg.rect(mouseX, mouseY, w, h);
979 vg.fill();
982 //vg.endFrame();
984 vg.endFrame();
985 glconDraw();
988 void processThreads () {
989 ReformatWorkComplete wd;
990 for (;;) {
991 bool workDone = false;
992 auto res = receiveTimeout(Duration.zero,
993 (QuitWork w) {
994 formatWorks = -1;
996 (ReformatWorkComplete w) {
997 wd = w;
998 workDone = true;
1001 if (!res) { assert(!workDone); break; }
1002 if (workDone) { workDone = false; formatComplete(wd); }
1006 auto lastTimerEventTime = MonoTime.currTime;
1007 bool somethingVisible = true;
1009 sdwindow.visibilityChanged = delegate (bool vis) {
1010 //import core.stdc.stdio; printf("VISCHANGED: %s\n", (vis ? "tan" : "ona").ptr);
1011 somethingVisible = vis;
1014 conRegVar!fpsVisible("r_fps", "show fps indicator", (self, valstr) { refresh(); });
1015 conRegVar!inGalaxyMap("r_galaxymap", "show Elite galaxy map", (self, valstr) { refresh(); });
1017 conRegVar!interAllowed("r_interference", "show interference", (self, valstr) { refresh(); });
1018 conRegVar!sbLeft("r_sbleft", "show scrollbar at the left side", (self, valstr) { refresh(); });
1020 conRegVar!showShip("r_showship", "show Elite ship", (self, valstr) {
1021 if (eliteShipFiles.length == 0) return false;
1022 return true;
1024 (self, valstr) {
1025 if (showShip) ensureShipModel(); else freeShipModel();
1026 refresh();
1030 conRegFunc!(() {
1031 if (currPopup !is null) {
1032 currPopup.destroy;
1033 currPopup = null;
1034 freeShipModel();
1035 refresh();
1037 onPopupSelect = null;
1038 })("menu_close", "close current popup menu");
1040 conRegFunc!(() {
1041 if (formatWorks == 0 && !showShip && !inGalaxyMap) {
1042 currPopup.destroy;
1043 currPopup = null;
1044 ensureShipModel();
1045 createSectionMenu();
1046 refresh();
1048 })("menu_section", "show section menu");
1050 conRegFunc!(() {
1051 if (formatWorks == 0 && !showShip && !inGalaxyMap) {
1052 currPopup.destroy;
1053 currPopup = null;
1054 ensureShipModel();
1055 createRecentMenu();
1056 refresh();
1058 })("menu_recent", "show recent menu");
1060 sdwindow.eventLoop(1000/34,
1061 delegate () {
1062 processThreads();
1063 if (sdwindow.closed) return;
1064 conProcessQueue();
1065 if (isQuitRequested) { closeWindow(); return; }
1066 auto ctt = MonoTime.currTime;
1069 auto spass = (ctt-lastTimerEventTime).total!"msecs";
1070 //if (spass >= 30) { import core.stdc.stdio; printf("WARNING: too long frame time: %u\n", cast(uint)spass); }
1071 //{ import core.stdc.stdio; printf("FRAME TIME: %u\n", cast(uint)spass); }
1072 lastTimerEventTime = ctt;
1073 // update FPS timer
1074 prevt = curt;
1075 //curt = MonoTime.currTime;
1076 curt = ctt;
1077 //auto secs = cast(double)((curt-stt).total!"msecs")/1000.0;
1078 auto dt = cast(double)((curt-prevt).total!"msecs")/1000.0;
1079 if (fps !is null) fps.update(dt);
1082 // smooth scrolling
1083 if (formatWorks == 0) {
1084 enum Delta = 92*2;
1085 if (toMove != 0) {
1086 import std.math : abs;
1087 immutable int sign = (toMove < 0 ? -1 : 1);
1088 // change speed
1089 if (arrowSpeed == 0) arrowSpeed = 16;
1090 if (abs(toMove) <= arrowSpeed) {
1091 arrowSpeed /= 2;
1092 if (arrowSpeed < 4) arrowSpeed = 4;
1093 } else {
1094 arrowSpeed *= 2;
1095 if (arrowSpeed > Delta) arrowSpeed = Delta;
1097 // calc move distance
1098 int sc = arrowSpeed;
1099 if (sc > abs(toMove)) sc = abs(toMove);
1100 hardScrollBy(sc*sign);
1101 toMove -= sc*sign;
1102 if (toMove == 0) arrowSpeed = 0;
1103 nextFadeTime = ctt+500.msecs;
1104 refresh();
1105 } else if (arrowDir) {
1106 if ((arrowDir < 0 && arrowSpeed > 0) || (arrowDir > 0 && arrowSpeed < 0)) arrowSpeed += arrowDir*4;
1107 arrowSpeed += arrowDir*2;
1108 if (arrowSpeed < -64) arrowSpeed = -64; else if (arrowSpeed > 64) arrowSpeed = 64;
1109 hardScrollBy(arrowSpeed);
1110 refresh();
1111 } else if (arrowSpeed != 0) {
1112 if (arrowSpeed < 0) {
1113 if ((arrowSpeed += 4) > 0) arrowSpeed = 0;
1114 } else {
1115 if ((arrowSpeed -= 4) < 0) arrowSpeed = 0;
1117 if (arrowSpeed) {
1118 hardScrollBy(arrowSpeed);
1119 refresh();
1122 // highlight fading
1123 if (newYFade) {
1124 if (ctt >= nextFadeTime) {
1125 if ((newYAlpha -= 0.1) <= 0) {
1126 newYFade = false;
1127 } else {
1128 nextFadeTime = ctt+25.msecs;
1130 refresh();
1134 // interference processing
1135 if (ctt >= nextIfTime) {
1136 import std.random : uniform;
1137 if (uniform!"[]"(0, 100) >= 50) { if (addIf()) refresh(); }
1138 nextIfTime += (uniform!"[]"(50, 1500)).msecs;
1140 if (processIfs()) refresh();
1141 // ship rotation
1142 if (shipModel !is null) {
1143 shipAngle -= 1;
1144 if (shipAngle < 359) shipAngle += 360;
1145 refresh();
1147 // mouse autohide
1148 if (!mouseFadingAway) {
1149 if (!mouseHidden && !mouseHigh) {
1150 if ((ctt-lastMMove).total!"msecs" > 2500) {
1151 //mouseHidden = true;
1152 mouseFadingAway = true;
1153 mouseAlpha = 1.0f;
1154 refresh();
1158 if (somethingVisible) {
1159 // sadly, to keep framerate we have to redraw each frame, or driver will throw us out of the ship
1160 if (/*needRedraw*/true) sdwindow.redrawOpenGlSceneNow();
1161 } else {
1162 refresh();
1164 doSaveState();
1165 // load new book?
1166 if (newBookFileName.length && formatWorks == 0 && vg !is null) {
1167 doSaveState(true); // forced state save
1168 closeMenu();
1169 ensureShipModel();
1170 loadAndFormat(newBookFileName);
1171 newBookFileName = null;
1172 sdwindow.redrawOpenGlSceneNow();
1173 //refresh();
1176 delegate (KeyEvent event) {
1177 if (sdwindow.closed) return;
1178 if (glconKeyEvent(event)) return;
1179 if (event.key == Key.PadEnter) event.key = Key.Enter;
1180 if ((event.modifierState&ModifierState.numLock) == 0) {
1181 switch (event.key) {
1182 case Key.Pad0: event.key = Key.Insert; break;
1183 case Key.PadDot: event.key = Key.Delete; break;
1184 case Key.Pad1: event.key = Key.End; break;
1185 case Key.Pad2: event.key = Key.Down; break;
1186 case Key.Pad3: event.key = Key.PageDown; break;
1187 case Key.Pad4: event.key = Key.Left; break;
1188 case Key.Pad6: event.key = Key.Right; break;
1189 case Key.Pad7: event.key = Key.Home; break;
1190 case Key.Pad8: event.key = Key.Up; break;
1191 case Key.Pad9: event.key = Key.PageUp; break;
1192 //case Key.PadEnter: event.key = Key.Enter; break;
1193 default:
1196 if (controlKey(event)) return;
1197 if (menuKey(event)) return;
1198 if (readerKey(event)) return;
1200 delegate (MouseEvent event) {
1201 if (sdwindow.closed) return;
1203 int linkAt (int msx, int msy) {
1204 if (laytext !is null && bookmeta !is null) {
1205 if (msx >= startX && msx <= endX && msy >= startY && msy <= endY) {
1206 auto widx = laytext.wordAtXY(msx-startX, topY+msy-startY);
1207 if (widx >= 0) {
1208 //conwriteln("word at (", msx-startX, ",", msy-startY, "): ", widx);
1209 auto w = laytext.wordByIndex(widx);
1210 while (widx >= 0) {
1211 //conwriteln("word #", widx, "; href=", w.style.href);
1212 if (!w.style.href) break;
1213 if (auto hr = w.wordNum in bookmeta.hrefs) {
1214 dstring href = hr.name;
1215 if (href.length > 1 && href[0] == '#') {
1216 href = href[1..$];
1217 foreach (const ref id; bookmeta.ids) {
1218 if (id.name == href) {
1219 //pushPosition();
1220 //goTo(id.wordidx);
1221 return id.wordidx;
1224 //conwriteln("id '", hr.name, "' not found!");
1225 return -1;
1228 --widx;
1229 --w;
1234 return -1;
1237 lastMMove = MonoTime.currTime;
1238 if (mouseHidden || mouseFadingAway) {
1239 mouseHidden = false;
1240 mouseFadingAway = false;
1241 mouseAlpha = 1.0f;
1242 refresh();
1244 if (mouseX != event.x || mouseY != event.y) {
1245 mouseX = event.x;
1246 mouseY = event.y;
1247 refresh();
1249 if (!menuMouse(event) && !showShip) {
1250 if (event.type == MouseEventType.buttonPressed) {
1251 switch (event.button) {
1252 case MouseButton.wheelUp: hardScrollBy(-42); break;
1253 case MouseButton.wheelDown: hardScrollBy(42); break;
1254 case MouseButton.left:
1255 auto wid = linkAt(event.x, event.y);
1256 if (wid >= 0) {
1257 pushPosition();
1258 goTo(wid);
1260 break;
1261 case MouseButton.right:
1262 popPosition();
1263 break;
1264 default:
1268 mouseHigh = (linkAt(event.x, event.y) >= 0);
1270 delegate (dchar ch) {
1271 if (sdwindow.closed) return;
1272 if (glconCharEvent(ch)) return;
1273 if (menuChar(ch)) return;
1274 //if (ch == '`') { concmd("r_console tan"); return; }
1277 closeWindow();
1279 childTid.send(QuitWork());
1280 while (formatWorks >= 0) processThreads();
1284 // ////////////////////////////////////////////////////////////////////////// //
1285 struct FlibustaUrl {
1286 string fullUrl; // onion
1287 string host;
1288 string id;
1290 this (const(char)[] aurl) {
1291 import std.format : format;
1292 aurl = aurl.xstrip();
1293 auto flibustaRE = regex(`^(?:https?://)?(?:www\.)?flibusta\.[^/]+/b/(\d+)`);
1294 auto ct = aurl.matchFirst(flibustaRE);
1295 if (!ct.empty) {
1296 fullUrl = "http://flibustahezeous3.onion/b/%s/fb2".format(ct[1]);
1297 id = ct[1].idup;
1298 host = "flibustahezeous3.onion";
1299 } else {
1300 // add protocol
1301 auto protoRE = regex(`^([^:/]+):`);
1302 auto protoMt = aurl.matchFirst(protoRE);
1303 if (protoMt.empty) fullUrl = "http:%s%s".format((aurl[0] == '/' ? "" : "//"), aurl);
1304 // add host
1305 auto hostRE = regex(`^(?:[^:/]+)://([^/]+)`);
1306 auto hostMt = fullUrl.matchFirst(hostRE);
1307 if (hostMt.empty) { fullUrl = null; return; }
1308 host = hostMt[1].idup;
1312 @property bool valid () const pure nothrow @safe @nogc { pragma(inline, true); return (fullUrl.length > 0); }
1313 @property bool isFlibusta () const pure nothrow @safe @nogc { pragma(inline, true); return (id.length > 0); }
1314 @property bool isOnion () const pure nothrow @safe @nogc { pragma(inline, true); return host.endsWithCI(".onion"); }
1318 // ////////////////////////////////////////////////////////////////////////// //
1319 __gshared SDBM dbCache = null;
1322 void dbOpenCache () {
1323 if (dbCache is null) {
1324 import std.file : exists, mkdirRecurse;
1325 import std.path;
1326 string xfn = buildPath(RcDir, "cache");
1327 xfn.mkdirRecurse();
1328 xfn = buildPath(xfn, ".cache.db");
1329 dbCache = new SDBM(xfn, SDBM.WRITER|SDBM.CREAT|SDBM.NOLCK);
1334 char[] dbBuildNamePathById (char[] dest, const(char)[] id) {
1335 import core.stdc.stdio : snprintf;
1336 assert(id.length > 0);
1337 assert(dest.length > 0);
1338 auto len = snprintf(dest.ptr, dest.length, "/files/%.*s/name", cast(uint)id.length, id.ptr);
1339 if (len < 1 || len >= dest.length) assert(0, "out of destination buffer");
1340 return dest[0..len];
1344 string dbFindInCache() (in auto ref FlibustaUrl furl) {
1345 if (!furl.valid) return null;
1346 dbOpenCache();
1347 char[128] keybuf = void;
1348 auto dbpath = dbBuildNamePathById(keybuf[], furl.id);
1349 string fname = dbCache.get!string(dbpath);
1350 if (fname.length == 0) return null;
1351 import std.file : exists;
1352 try {
1353 if (fname.exists) return fname;
1354 } catch (Exception e) {}
1355 // no such file, remove it from database
1356 dbCache.del(dbpath);
1357 return null;
1361 void dbPutToCache() (in auto ref FlibustaUrl furl, const(char)[] fname) {
1362 if (!furl.valid || fname.length == 0) return;
1363 dbOpenCache();
1364 char[128] keybuf = void;
1365 dbCache.put(dbBuildNamePathById(keybuf[], furl.id), fname);
1369 void dbCloseCache () {
1370 if (dbCache is null) return;
1371 scope(exit) {
1372 delete dbCache;
1373 import core.memory : GC;
1374 GC.collect();
1375 GC.minimize();
1380 void dbCleanupCache () {
1381 dbOpenCache();
1382 bool[string] dead;
1383 auto xre = regex(`^/files/(.+)/name$`);
1384 dbCache.itInit();
1385 nextkey: for (;;) {
1386 auto key = dbCache.itNext();
1387 if (key is null) break;
1388 //writeln("[", key, "]");
1389 auto mt = key.matchFirst(xre);
1390 if (mt.empty) continue;
1391 auto fname = dbCache.get!string(key);
1392 if (fname.length != 0) {
1393 try {
1394 import std.file : exists;
1395 if (fname.exists) continue nextkey;
1396 } catch (Exception e) {}
1398 dead[key.idup] = true; // oops
1400 foreach (string k; dead.byKey) {
1401 //writeln("deleting stale cache record for '", k, "'");
1402 dbCache.del(k);
1404 dead.clear();
1408 // ////////////////////////////////////////////////////////////////////////// //
1409 // returns file name
1410 string fileDown() (in auto ref FlibustaUrl furl) {
1411 if (!furl.valid || !furl.isFlibusta) return null;
1413 string cachedFName = dbFindInCache(furl);
1414 if (cachedFName.length) return cachedFName;
1416 // content-disposition: attachment; filename="Divov_Sled-zombi.1lzb6Q.96382.fb2.zip"
1417 auto cdRE0 = regex(`^\s*attachment\s*;\s*filename="(.+?)"`, "i");
1418 auto cdRE1 = regex(`^\s*attachment\s*;\s*filename=([^;]+?)`, "i");
1420 auto http = HTTP(furl.host);
1421 http.method = HTTP.Method.get;
1422 http.url = furl.fullUrl;
1424 string fname = null;
1425 string tmpfname = null;
1426 string fnps = null;
1427 bool alreadyDowned = false;
1428 VFile fo;
1430 http.onReceiveHeader = delegate (in char[] key, in char[] value) {
1431 //writeln(key ~ ": " ~ value);
1432 if (key.strEquCI("content-disposition")) {
1433 auto ct = value.matchFirst(cdRE0);
1434 if (ct.empty) ct = value.matchFirst(cdRE1);
1435 if (ct[1].length) {
1436 auto fnp = ct[1].xstrip;
1437 auto lslpos = fnp.lastIndexOf('/');
1438 if (lslpos > 0) fnp = fnp[lslpos+1..$];
1439 if (fnp.length == 0) {
1440 fname = null;
1441 } else {
1442 import std.file : exists, mkdirRecurse;
1443 import std.path;
1444 string xfn = buildPath(RcDir, "cache");
1445 xfn.mkdirRecurse();
1446 char[] xxname;
1447 xxname.reserve(fnp.length);
1448 foreach (char ch; fnp) {
1449 if (ch <= ' ' || ch == 127) ch = '_';
1450 xxname ~= ch;
1452 fnps = cast(string)xxname; // it is safe to cast here
1453 fname = buildPath(xfn, fnps);
1454 tmpfname = fname~".down.part";
1456 if (fname.exists) {
1457 alreadyDowned = true;
1458 throw new Exception("already here");
1459 //throw new FileAlreadyDowned("already here");
1462 //write("\r", fnp, " [", furl.fullUrl, "]\e[K");
1468 http.onReceive = delegate (ubyte[] data) {
1469 if (!fo.isOpen) {
1470 if (fname.length == 0) throw new Exception("no file name found in headers");
1471 //writeln(" downloading to ", fname);
1472 fo = VFile(tmpfname, "w");
1474 fo.rawWriteExact(data);
1475 return data.length;
1478 MonoTime lastProgTime = MonoTime.zero;
1479 enum BarLength = 68;
1480 bool doProgUpdate = true;
1481 char[1024] buf = void;
1482 int oldDots = -1, oldPrc = -1;
1483 uint bufpos = 0;
1484 int stickPos = 1;
1485 immutable string stickStr = `|/-\`;
1487 // will set `doProgUpdate`, and update `oldXXX`
1488 void buildPBar (usize dlTotal, usize dlNow) {
1489 void put (const(char)[] s...) nothrow {
1490 if (s.length == 0) return;
1491 if (bufpos >= buf.length) return;
1492 int left = cast(int)buf.length-bufpos;
1493 if (s.length > left) s = s[0..left];
1494 assert(s.length > 0);
1495 import core.stdc.string : memcpy;
1496 memcpy(buf.ptr+bufpos, s.ptr, s.length);
1497 bufpos += cast(int)s.length;
1499 void putprc (int prc) {
1500 if (prc < 0) prc = 0; else if (prc > 100) prc = 100;
1501 if (bufpos >= buf.length || buf.length-bufpos < 5) return; // oops
1502 import core.stdc.stdio;
1503 bufpos += cast(int)snprintf(buf.ptr+bufpos, 5, "%3d%%", prc);
1505 void putCommaNum (usize n, usize max=0) {
1506 char[128] buf = void;
1507 if (max < n) {
1508 put(intWithCommas(buf[], n));
1509 } else {
1510 auto len = intWithCommas(buf[], max).length;
1511 auto pt = intWithCommas(buf[], n);
1512 while (len-- > pt.length) put(" ");
1513 put(pt);
1516 bufpos = 0;
1517 put("\r");
1518 put(fnps);
1519 put(" [");
1520 auto barpos = bufpos;
1521 foreach (immutable _; 0..BarLength) put(" ");
1522 put("]");
1523 if (dlTotal > 0) {
1524 int prc = cast(int)(cast(ulong)100*dlNow/dlTotal);
1525 if (prc < 0) prc = 0; else if (prc > 100) prc = 100;
1526 int dots = cast(int)(cast(ulong)BarLength*dlNow/dlTotal);
1527 if (dots < 0) dots = 0; else if (dots > BarLength) dots = BarLength;
1528 if (prc != oldPrc || dots != oldDots) {
1529 doProgUpdate = true;
1530 oldPrc = prc;
1531 oldDots = dots;
1533 put(" [");
1534 putCommaNum(dlNow, dlTotal);
1535 put("/");
1536 putCommaNum(dlTotal);
1537 put("] ");
1538 putprc(prc);
1539 // dots
1540 foreach (immutable dp; 0..dots) if (barpos+dp < buf.length) buf[barpos+dp] = '.';
1541 } else {
1542 put("?\e[K");
1543 if (oldDots != -1 || oldPrc != -1) doProgUpdate = true;
1544 oldDots = -1;
1545 oldPrc = -1;
1549 http.onProgress = delegate (usize dltotal, usize dlnow, usize ultotal, usize ulnow) {
1550 //writeln("Progress ", dltotal, ", ", dlnow, ", ", ultotal, ", ", ulnow);
1551 if (fname.length == 0) {
1552 auto ct = MonoTime.currTime;
1553 if ((ct-lastProgTime).total!"msecs" >= 100) {
1554 write("\x08", stickStr[stickPos]);
1555 stickPos = (stickPos+1)%cast(int)stickStr.length;
1556 lastProgTime = ct;
1558 return 0;
1560 buildPBar(dltotal, dlnow);
1561 if (doProgUpdate) { write("\e[?7l", buf[0..bufpos], "\e[K\e[?7h"); doProgUpdate = false; }
1562 //if (dltotal == 0) return 0;
1563 //auto ct = MonoTime.currTime;
1564 //if ((ct-lastProgTime).total!"msecs" < 1000) return 0;
1565 //lastProgTime = ct;
1566 //writef("\r%s [%s] -- [%s/%s] %3u%%\e[K", fnps, host, intWithCommas(dlnow), intWithCommas(dltotal), 100UL*dlnow/dltotal);
1567 return 0;
1570 if (furl.isOnion) {
1571 http.proxyType = HTTP.CurlProxy.socks5_hostname;
1572 http.proxy = "127.0.0.1";
1573 http.proxyPort = 9050;
1576 try {
1577 //write("downloading from [", host, "]: ", realUrl, " ... ");
1578 write("downloading from Flibusta: ", furl.fullUrl, " ... ", stickStr[0]);
1579 http.perform();
1580 if (fo.isOpen) {
1581 buildPBar(cast(uint)fo.size, cast(uint)fo.size);
1582 } else {
1583 buildPBar(1, 1);
1585 writeln(buf[0..bufpos], "\e[K");
1586 //write("\r\e[K");
1587 } catch (Exception e) {
1588 if (/*cast(FileAlreadyDowned)e*/alreadyDowned) {
1589 write("\r", fname, " already downloaded.\e[K");
1590 return fname; // already here
1592 if (tmpfname.length) {
1593 import std.exception : collectException;
1594 import std.file : remove;
1595 collectException(tmpfname.remove);
1597 throw e;
1600 if (fo.isOpen) {
1601 // something was downloaded, rename it
1602 import std.file : rename;
1603 fo.close();
1604 rename(tmpfname, fname);
1605 dbPutToCache(furl, fname);
1606 return fname;
1609 return null;
1613 // ////////////////////////////////////////////////////////////////////////// //
1614 void main (string[] args) {
1615 import std.path;
1617 conRegVar!oglConScale(1, 4, "r_conscale", "console scale");
1619 conProcessQueue(256*1024); // load config
1620 conProcessArgs!true(args);
1621 conProcessQueue(256*1024);
1623 universe = Galaxy(0);
1625 if (args.length == 1) {
1626 try {
1627 string lnn;
1628 foreach (string line; VFile(buildPath(RcDir, ".lastfile")).byLineCopy) {
1629 if (!line.isComment) lnn = line;
1631 if (lnn.length) args ~= lnn;
1632 } catch (Exception) {}
1633 } else {
1634 if (args.length != 2) assert(0, "invalid number of arguments");
1635 auto furl = FlibustaUrl(args[1]);
1636 if (furl.valid && furl.isFlibusta) {
1637 dbCleanupCache();
1638 scope(exit) dbCloseCache();
1639 string fn = fileDown(furl);
1640 if (fn.length == 0) assert(0, "can't download file");
1641 args[1] = fn;
1645 if (args.length == 1) assert(0, "no filename");
1647 readConfig();
1649 bookFileName = args[1];
1650 run();