1 /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2 
3 Redistribution and use in source and binary forms, with or without
4 modification, are permitted provided that the following conditions are met:
5 
6 1. Redistributions of source code must retain the above copyright notice, this
7    list of conditions and the following disclaimer.
8 2. Redistributions in binary form must reproduce the above copyright notice,
9    this list of conditions and the following disclaimer in the documentation
10    and/or other materials provided with the distribution.
11 
12 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 
23 #include "app.h"
24 #include "bookmarks.h"
25 #include "defs.h"
26 #include "embedded.h"
27 #include "feeds.h"
28 #include "mimehooks.h"
29 #include "gmcerts.h"
30 #include "gmdocument.h"
31 #include "gmutil.h"
32 #include "history.h"
33 #include "ipc.h"
34 #include "periodic.h"
35 #include "sitespec.h"
36 #include "ui/certimportwidget.h"
37 #include "ui/color.h"
38 #include "ui/command.h"
39 #include "ui/documentwidget.h"
40 #include "ui/inputwidget.h"
41 #include "ui/keys.h"
42 #include "ui/labelwidget.h"
43 #include "ui/root.h"
44 #include "ui/sidebarwidget.h"
45 #include "ui/uploadwidget.h"
46 #include "ui/text.h"
47 #include "ui/util.h"
48 #include "ui/window.h"
49 #include "visited.h"
50 
51 #include <the_Foundation/commandline.h>
52 #include <the_Foundation/file.h>
53 #include <the_Foundation/fileinfo.h>
54 #include <the_Foundation/path.h>
55 #include <the_Foundation/process.h>
56 #include <the_Foundation/sortedarray.h>
57 #include <the_Foundation/time.h>
58 #include <SDL.h>
59 
60 #include <stdio.h>
61 #include <stdarg.h>
62 #include <errno.h>
63 
64 //#define LAGRANGE_ENABLE_MOUSE_TOUCH_EMULATION 1
65 
66 #if defined (iPlatformAppleDesktop)
67 #   include "macos.h"
68 #endif
69 #if defined (iPlatformAppleMobile)
70 #   include "ios.h"
71 #endif
72 #if defined (iPlatformMsys)
73 #   include "win32.h"
74 #endif
75 #if SDL_VERSION_ATLEAST(2, 0, 14)
76 #   include <SDL_misc.h>
77 #endif
78 
79 iDeclareType(App)
80 
81 #if defined (iPlatformAppleDesktop)
82 #define EMB_BIN "../../Resources/resources.lgr"
83 static const char *defaultDataDir_App_ = "~/Library/Application Support/fi.skyjake.Lagrange";
84 #endif
85 #if defined (iPlatformAppleMobile)
86 #define EMB_BIN "../../Resources/resources.lgr"
87 static const char *defaultDataDir_App_ = "~/Library/Application Support";
88 #endif
89 #if defined (iPlatformMsys)
90 #define EMB_BIN "../resources.lgr"
91 static const char *defaultDataDir_App_ = "~/AppData/Roaming/fi.skyjake.Lagrange";
92 #endif
93 #if defined (iPlatformLinux) || defined (iPlatformOther)
94 #define EMB_BIN  "../../share/lagrange/resources.lgr"
95 static const char *defaultDataDir_App_ = "~/.config/lagrange";
96 #endif
97 #if defined (iPlatformHaiku)
98 #define EMB_BIN "./resources.lgr"
99 static const char *defaultDataDir_App_ = "~/config/settings/lagrange";
100 #endif
101 #if defined (LAGRANGE_EMB_BIN) /* specified in build config */
102 #  undef EMB_BIN
103 #  define EMB_BIN LAGRANGE_EMB_BIN
104 #endif
105 #define EMB_BIN2 "../resources.lgr" /* fallback from build/executable dir */
106 static const char *prefsFileName_App_      = "prefs.cfg";
107 static const char *oldStateFileName_App_   = "state.binary";
108 static const char *stateFileName_App_      = "state.lgr";
109 static const char *defaultDownloadDir_App_ = "~/Downloads";
110 
111 static const int idleThreshold_App_ = 1000; /* ms */
112 
113 struct Impl_App {
114     iCommandLine args;
115     iString *    execPath;
116     iMimeHooks * mimehooks;
117     iGmCerts *   certs;
118     iVisited *   visited;
119     iBookmarks * bookmarks;
120     iMainWindow *window;
121     iPtrArray    popupWindows;
122     iSortedArray tickers; /* per-frame callbacks, used for animations */
123     uint32_t     lastTickerTime;
124     uint32_t     elapsedSinceLastTicker;
125     iBool        isRunning;
126     iBool        isRunningUnderWindowSystem;
127 #if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
128     iBool        isIdling;
129     uint32_t     lastEventTime;
130     int          sleepTimer;
131 #endif
132     iAtomicInt   pendingRefresh;
133     iBool        isLoadingPrefs;
134     iStringList *launchCommands;
135     iBool        isFinishedLaunching;
136     iTime        lastDropTime; /* for detecting drops of multiple items */
137     int          autoReloadTimer;
138     iPeriodic    periodic;
139     int          warmupFrames; /* forced refresh just after resuming from background; FIXME: shouldn't be needed */
140     /* Preferences: */
141     iBool        commandEcho;         /* --echo */
142     iBool        forceSoftwareRender; /* --sw */
143     iRect        initialWindowRect;
144     iPrefs       prefs;
145 };
146 
147 static iApp app_;
148 
149 /*----------------------------------------------------------------------------------------------*/
150 
151 iDeclareType(Ticker)
152 
153 struct Impl_Ticker {
154     iAny *context;
155     iRoot *root;
156     void (*callback)(iAny *);
157 };
158 
cmp_Ticker_(const void * a,const void * b)159 static int cmp_Ticker_(const void *a, const void *b) {
160     const iTicker *elems[2] = { a, b };
161     return iCmp(elems[0]->context, elems[1]->context);
162 }
163 
164 /*----------------------------------------------------------------------------------------------*/
165 
dateStr_(const iDate * date)166 const iString *dateStr_(const iDate *date) {
167     return collectNewFormat_String("%d-%02d-%02d %02d:%02d:%02d",
168                                    date->year,
169                                    date->month,
170                                    date->day,
171                                    date->hour,
172                                    date->minute,
173                                    date->second);
174 }
175 
serializePrefs_App_(const iApp * d)176 static iString *serializePrefs_App_(const iApp *d) {
177     iString *str = new_String();
178 #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
179     appendFormat_String(str, "customframe arg:%d\n", d->prefs.customFrame);
180 #endif
181     appendFormat_String(str, "window.retain arg:%d\n", d->prefs.retainWindowSize);
182     if (d->prefs.retainWindowSize) {
183         int w, h, x, y;
184         x = d->window->place.normalRect.pos.x;
185         y = d->window->place.normalRect.pos.y;
186         w = d->window->place.normalRect.size.x;
187         h = d->window->place.normalRect.size.y;
188         appendFormat_String(str, "window.setrect width:%d height:%d coord:%d %d\n", w, h, x, y);
189         /* On macOS, maximization should be applied at creation time or the window will take
190            a moment to animate to its maximized size. */
191 #if defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
192         if (snap_MainWindow(d->window)) {
193             if (snap_MainWindow(d->window) == maximized_WindowSnap) {
194                 appendFormat_String(str, "~window.maximize\n");
195             }
196             else if (~SDL_GetWindowFlags(d->window->base.win) & SDL_WINDOW_MINIMIZED) {
197                 /* Save the actual visible window position, too, because snapped windows may
198                    still be resized/moved without affecting normalRect. */
199                 SDL_GetWindowPosition(d->window->base.win, &x, &y);
200                 SDL_GetWindowSize(d->window->base.win, &w, &h);
201                 appendFormat_String(
202                     str, "~window.setrect snap:%d width:%d height:%d coord:%d %d\n",
203                     snap_MainWindow(d->window), w, h, x, y);
204             }
205         }
206 #elif !defined (iPlatformApple)
207         if (snap_MainWindow(d->window) == maximized_WindowSnap) {
208             appendFormat_String(str, "~window.maximize\n");
209         }
210 #endif
211     }
212     appendFormat_String(str, "uilang id:%s\n", cstr_String(&d->prefs.uiLanguage));
213     appendFormat_String(str, "uiscale arg:%f\n", uiScale_Window(as_Window(d->window)));
214     appendFormat_String(str, "prefs.dialogtab arg:%d\n", d->prefs.dialogTab);
215     appendFormat_String(str, "font.set arg:%d\n", d->prefs.font);
216     appendFormat_String(str, "font.user path:%s\n", cstr_String(&d->prefs.symbolFontPath));
217     appendFormat_String(str, "headingfont.set arg:%d\n", d->prefs.headingFont);
218     appendFormat_String(str, "zoom.set arg:%d\n", d->prefs.zoomPercent);
219     appendFormat_String(str, "smoothscroll arg:%d\n", d->prefs.smoothScrolling);
220     appendFormat_String(str, "scrollspeed arg:%d type:%d\n", d->prefs.smoothScrollSpeed[keyboard_ScrollType], keyboard_ScrollType);
221     appendFormat_String(str, "scrollspeed arg:%d type:%d\n", d->prefs.smoothScrollSpeed[mouse_ScrollType], mouse_ScrollType);
222     appendFormat_String(str, "imageloadscroll arg:%d\n", d->prefs.loadImageInsteadOfScrolling);
223     appendFormat_String(str, "cachesize.set arg:%d\n", d->prefs.maxCacheSize);
224     appendFormat_String(str, "memorysize.set arg:%d\n", d->prefs.maxMemorySize);
225     appendFormat_String(str, "decodeurls arg:%d\n", d->prefs.decodeUserVisibleURLs);
226     appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth);
227     appendFormat_String(str, "linespacing.set arg:%f\n", d->prefs.lineSpacing);
228     appendFormat_String(str, "returnkey.set arg:%d\n", d->prefs.returnKey);
229     /* TODO: Set up an array of booleans in Prefs and do these in a loop. */
230     appendFormat_String(str, "prefs.animate.changed arg:%d\n", d->prefs.uiAnimations);
231     appendFormat_String(str, "prefs.mono.gemini.changed arg:%d\n", d->prefs.monospaceGemini);
232     appendFormat_String(str, "prefs.mono.gopher.changed arg:%d\n", d->prefs.monospaceGopher);
233     appendFormat_String(str, "prefs.boldlink.dark.changed arg:%d\n", d->prefs.boldLinkDark);
234     appendFormat_String(str, "prefs.boldlink.light.changed arg:%d\n", d->prefs.boldLinkLight);
235     appendFormat_String(str, "prefs.biglede.changed arg:%d\n", d->prefs.bigFirstParagraph);
236     appendFormat_String(str, "prefs.plaintext.wrap.changed arg:%d\n", d->prefs.plainTextWrap);
237     appendFormat_String(str, "prefs.sideicon.changed arg:%d\n", d->prefs.sideIcon);
238     appendFormat_String(str, "prefs.centershort.changed arg:%d\n", d->prefs.centerShortDocs);
239     appendFormat_String(str, "prefs.collapsepreonload.changed arg:%d\n", d->prefs.collapsePreOnLoad);
240     appendFormat_String(str, "prefs.hoverlink.changed arg:%d\n", d->prefs.hoverLink);
241     appendFormat_String(str, "prefs.bookmarks.addbottom.changed arg:%d\n", d->prefs.addBookmarksToBottom);
242     appendFormat_String(str, "prefs.archive.openindex.changed arg:%d\n", d->prefs.openArchiveIndexPages);
243     appendFormat_String(str, "quoteicon.set arg:%d\n", d->prefs.quoteIcon ? 1 : 0);
244     appendFormat_String(str, "theme.set arg:%d auto:1\n", d->prefs.theme);
245     appendFormat_String(str, "accent.set arg:%d\n", d->prefs.accent);
246     appendFormat_String(str, "ostheme arg:%d\n", d->prefs.useSystemTheme);
247     appendFormat_String(str, "doctheme.dark.set arg:%d\n", d->prefs.docThemeDark);
248     appendFormat_String(str, "doctheme.light.set arg:%d\n", d->prefs.docThemeLight);
249     appendFormat_String(str, "saturation.set arg:%d\n", (int) ((d->prefs.saturation * 100) + 0.5f));
250     appendFormat_String(str, "imagestyle.set arg:%d\n", d->prefs.imageStyle);
251     appendFormat_String(str, "ca.file noset:1 path:%s\n", cstr_String(&d->prefs.caFile));
252     appendFormat_String(str, "ca.path path:%s\n", cstr_String(&d->prefs.caPath));
253     appendFormat_String(str, "proxy.gemini address:%s\n", cstr_String(&d->prefs.geminiProxy));
254     appendFormat_String(str, "proxy.gopher address:%s\n", cstr_String(&d->prefs.gopherProxy));
255     appendFormat_String(str, "proxy.http address:%s\n", cstr_String(&d->prefs.httpProxy));
256 #if defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT)
257     appendFormat_String(str, "downloads path:%s\n", cstr_String(&d->prefs.downloadDir));
258 #endif
259     appendFormat_String(str, "searchurl address:%s\n", cstr_String(&d->prefs.searchUrl));
260     appendFormat_String(str, "translation.languages from:%d to:%d\n", d->prefs.langFrom, d->prefs.langTo);
261     return str;
262 }
263 
dataDir_App_(void)264 static const char *dataDir_App_(void) {
265 #if defined (iPlatformLinux) || defined (iPlatformOther)
266     const char *configHome = getenv("XDG_CONFIG_HOME");
267     if (configHome) {
268         return concatPath_CStr(configHome, "lagrange");
269     }
270 #endif
271 #if defined (iPlatformMsys)
272     /* Check for a portable userdata directory. */
273     iApp *d = &app_;
274     const char *userDir = concatPath_CStr(cstr_String(d->execPath), "..\\userdata");
275     if (fileExistsCStr_FileInfo(userDir)) {
276         return userDir;
277     }
278 #endif
279     return defaultDataDir_App_;
280 }
281 
downloadDir_App_(void)282 static const char *downloadDir_App_(void) {
283 #if defined (iPlatformLinux) || defined (iPlatformOther)
284     /* Parse user-dirs.dirs using the `xdg-user-dir` tool. */
285     iProcess *proc = iClob(new_Process());
286     setArguments_Process(
287         proc, iClob(newStringsCStr_StringList("/usr/bin/env", "xdg-user-dir", "DOWNLOAD", NULL)));
288     if (start_Process(proc)) {
289         iString *path = collect_String(newLocal_String(collect_Block(
290             readOutputUntilClosed_Process(proc))));
291         trim_String(path);
292         if (!isEmpty_String(path)) {
293             return cstr_String(path);
294         }
295     }
296 #endif
297 #if defined (iPlatformAppleMobile)
298     /* Save to a local cache directory from where the user can export to the cloud. */
299     const iString *dlDir = cleanedCStr_Path("~/Library/Caches/Downloads");
300     if (!fileExists_FileInfo(dlDir)) {
301         makeDirs_Path(dlDir);
302     }
303     return cstr_String(dlDir);
304 #endif
305     return defaultDownloadDir_App_;
306 }
307 
prefsFileName_(void)308 static const iString *prefsFileName_(void) {
309     return collectNewCStr_String(concatPath_CStr(dataDir_App_(), prefsFileName_App_));
310 }
311 
loadPrefs_App_(iApp * d)312 static void loadPrefs_App_(iApp *d) {
313     iUnused(d);
314     iBool haveCA = iFalse;
315     d->isLoadingPrefs = iTrue; /* affects which notifications get posted */
316     /* Create the data dir if it doesn't exist yet. */
317     makeDirs_Path(collectNewCStr_String(dataDir_App_()));
318     iFile *f = new_File(prefsFileName_());
319     if (open_File(f, readOnly_FileMode | text_FileMode)) {
320         iString *str = readString_File(f);
321         const iRangecc src = range_String(str);
322         iRangecc line = iNullRange;
323         while (nextSplit_Rangecc(src, "\n", &line)) {
324             iString cmdStr;
325             initRange_String(&cmdStr, line);
326             const char *cmd = cstr_String(&cmdStr);
327             /* Window init commands must be handled before the window is created. */
328             if (equal_Command(cmd, "uiscale")) {
329                 setUiScale_Window(get_Window(), argf_Command(cmd));
330             }
331             else if (equal_Command(cmd, "uilang")) {
332                 const char *id = cstr_Rangecc(range_Command(cmd, "id"));
333                 setCStr_String(&d->prefs.uiLanguage, id);
334                 setCurrent_Lang(id);
335             }
336             else if (equal_Command(cmd, "ca.file") || equal_Command(cmd, "ca.path")) {
337                 /* Background requests may be started before these commands would get
338                    handled via the event loop. */
339                 handleCommand_App(cmd);
340                 haveCA = iTrue;
341             }
342             else if (equal_Command(cmd, "customframe")) {
343                 d->prefs.customFrame = arg_Command(cmd);
344             }
345             else if (equal_Command(cmd, "window.setrect") && !argLabel_Command(cmd, "snap")) {
346                 const iInt2 pos = coord_Command(cmd);
347                 d->initialWindowRect = init_Rect(
348                     pos.x, pos.y, argLabel_Command(cmd, "width"), argLabel_Command(cmd, "height"));
349             }
350 #if !defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT)
351             else if (equal_Command(cmd, "downloads")) {
352                 continue; /* can't change downloads directory */
353             }
354 #endif
355             else {
356                 postCommandString_Root(NULL, &cmdStr);
357             }
358             deinit_String(&cmdStr);
359         }
360         delete_String(str);
361     }
362     if (!haveCA) {
363         /* Default CA setup. */
364         setCACertificates_TlsRequest(&d->prefs.caFile, &d->prefs.caPath);
365     }
366 #if !defined (LAGRANGE_ENABLE_CUSTOM_FRAME)
367     d->prefs.customFrame = iFalse;
368 #endif
369     iRelease(f);
370     d->isLoadingPrefs = iFalse;
371 }
372 
savePrefs_App_(const iApp * d)373 static void savePrefs_App_(const iApp *d) {
374     iString *cfg = serializePrefs_App_(d);
375     iFile *f = new_File(prefsFileName_());
376     if (open_File(f, writeOnly_FileMode | text_FileMode)) {
377         write_File(f, &cfg->chars);
378     }
379     iRelease(f);
380     delete_String(cfg);
381 }
382 
383 static const char *magicState_App_       = "lgL1";
384 static const char *magicWindow_App_      = "wind";
385 static const char *magicTabDocument_App_ = "tabd";
386 static const char *magicSidebar_App_     = "side";
387 
388 enum iDocumentStateFlag {
389     current_DocumentStateFlag     = iBit(1),
390     rootIndex1_DocumentStateFlag = iBit(2)
391 };
392 
loadState_App_(iApp * d)393 static iBool loadState_App_(iApp *d) {
394     iUnused(d);
395     const char *oldPath = concatPath_CStr(dataDir_App_(), oldStateFileName_App_);
396     const char *path    = concatPath_CStr(dataDir_App_(), stateFileName_App_);
397     iFile *f = iClob(newCStr_File(fileExistsCStr_FileInfo(path) ? path : oldPath));
398     if (open_File(f, readOnly_FileMode)) {
399         char magic[4];
400         readData_File(f, 4, magic);
401         if (memcmp(magic, magicState_App_, 4)) {
402             printf("%s: format not recognized\n", cstr_String(path_File(f)));
403             return iFalse;
404         }
405         const uint32_t version = readU32_File(f);
406         /* Check supported versions. */
407         if (version > latest_FileVersion) {
408             printf("%s: unsupported version\n", cstr_String(path_File(f)));
409             return iFalse;
410         }
411         setVersion_Stream(stream_File(f), version);
412         /* Window state. */
413         iDocumentWidget *doc           = NULL;
414         iDocumentWidget *current[2]    = { NULL, NULL };
415         iBool            isFirstTab[2] = { iTrue, iTrue };
416         while (!atEnd_File(f)) {
417             readData_File(f, 4, magic);
418             if (!memcmp(magic, magicWindow_App_, 4)) {
419                 const int splitMode = read32_File(f);
420                 const int keyRoot   = read32_File(f);
421                 d->window->pendingSplitMode = splitMode;
422                 setSplitMode_MainWindow(d->window, splitMode | noEvents_WindowSplit);
423                 d->window->base.keyRoot = d->window->base.roots[keyRoot];
424             }
425             else if (!memcmp(magic, magicSidebar_App_, 4)) {
426                 const uint16_t bits = readU16_File(f);
427                 const uint8_t modes = readU8_File(f);
428                 const float widths[2] = {
429                     readf_Stream(stream_File(f)),
430                     readf_Stream(stream_File(f))
431                 };
432                 iIntSet *closedFolders[2] = {
433                     collectNew_IntSet(),
434                     collectNew_IntSet()
435                 };
436                 if (version >= bookmarkFolderState_FileVersion) {
437                     deserialize_IntSet(closedFolders[0], stream_File(f));
438                     deserialize_IntSet(closedFolders[1], stream_File(f));
439                 }
440                 const uint8_t rootIndex = bits & 0xff;
441                 const uint8_t flags     = bits >> 8;
442                 iRoot *root = d->window->base.roots[rootIndex];
443                 if (root) {
444                     iSidebarWidget *sidebar  = findChild_Widget(root->widget, "sidebar");
445                     iSidebarWidget *sidebar2 = findChild_Widget(root->widget, "sidebar2");
446                     setClosedFolders_SidebarWidget(sidebar, closedFolders[0]);
447                     setClosedFolders_SidebarWidget(sidebar2, closedFolders[1]);
448                     postCommandf_Root(root, "sidebar.mode arg:%u", modes & 0xf);
449                     postCommandf_Root(root, "sidebar2.mode arg:%u", modes >> 4);
450                     if (deviceType_App() != phone_AppDeviceType) {
451                         setWidth_SidebarWidget(sidebar,  widths[0]);
452                         setWidth_SidebarWidget(sidebar2, widths[1]);
453                         if (flags & 1) postCommand_Root(root, "sidebar.toggle noanim:1");
454                         if (flags & 2) postCommand_Root(root, "sidebar2.toggle noanim:1");
455                     }
456                 }
457             }
458             else if (!memcmp(magic, magicTabDocument_App_, 4)) {
459                 const int8_t flags = read8_File(f);
460                 int rootIndex = flags & rootIndex1_DocumentStateFlag ? 1 : 0;
461                 if (rootIndex > numRoots_Window(as_Window(d->window)) - 1) {
462                     rootIndex = 0;
463                 }
464                 setCurrent_Root(d->window->base.roots[rootIndex]);
465                 if (isFirstTab[rootIndex]) {
466                     isFirstTab[rootIndex] = iFalse;
467                     /* There is one pre-created tab in each root. */
468                     doc = document_Root(get_Root());
469                 }
470                 else {
471                     doc = newTab_App(NULL, iFalse /* no switching */);
472                 }
473                 if (flags & current_DocumentStateFlag) {
474                     current[rootIndex] = doc;
475                 }
476                 deserializeState_DocumentWidget(doc, stream_File(f));
477                 doc = NULL;
478             }
479             else {
480                 printf("%s: unrecognized data\n", cstr_String(path_File(f)));
481                 setCurrent_Root(NULL);
482                 return iFalse;
483             }
484         }
485         if (d->window->splitMode) {
486             /* Update root placement. */
487             resize_MainWindow(d->window, -1, -1);
488         }
489         iForIndices(i, current) {
490             postCommandf_Root(NULL, "tabs.switch page:%p", current[i]);
491         }
492         setCurrent_Root(NULL);
493         return iTrue;
494     }
495     return iFalse;
496 }
497 
saveState_App_(const iApp * d)498 static void saveState_App_(const iApp *d) {
499     iUnused(d);
500     trimCache_App();
501     iMainWindow *win = d->window;
502     /* UI state is saved in binary because it is quite complex (e.g.,
503        navigation history, cached content) and depends closely on the widget
504        tree. The data is largely not reorderable and should not be modified
505        by the user manually. */
506     iFile *f = newCStr_File(concatPath_CStr(dataDir_App_(), stateFileName_App_));
507     if (open_File(f, writeOnly_FileMode)) {
508         writeData_File(f, magicState_App_, 4);
509         writeU32_File(f, latest_FileVersion); /* version */
510         /* Begin with window state. */ {
511             writeData_File(f, magicWindow_App_, 4);
512             writeU32_File(f, win->splitMode);
513             writeU32_File(f, win->base.keyRoot == win->base.roots[0] ? 0 : 1);
514         }
515         /* State of UI elements. */ {
516             iForIndices(i, win->base.roots) {
517                 const iRoot *root = win->base.roots[i];
518                 if (root) {
519                     writeData_File(f, magicSidebar_App_, 4);
520                     const iSidebarWidget *sidebar  = findChild_Widget(root->widget, "sidebar");
521                     const iSidebarWidget *sidebar2 = findChild_Widget(root->widget, "sidebar2");
522                     writeU16_File(f, i |
523                                   (isVisible_Widget(sidebar)  ? 0x100 : 0) |
524                                   (isVisible_Widget(sidebar2) ? 0x200 : 0));
525                     writeU8_File(f,
526                                  mode_SidebarWidget(sidebar) |
527                                  (mode_SidebarWidget(sidebar2) << 4));
528                     writef_Stream(stream_File(f), width_SidebarWidget(sidebar));
529                     writef_Stream(stream_File(f), width_SidebarWidget(sidebar2));
530                     serialize_IntSet(closedFolders_SidebarWidget(sidebar), stream_File(f));
531                     serialize_IntSet(closedFolders_SidebarWidget(sidebar2), stream_File(f));
532                 }
533             }
534         }
535         iConstForEach(ObjectList, i, iClob(listDocuments_App(NULL))) {
536             iAssert(isInstance_Object(i.object, &Class_DocumentWidget));
537             const iWidget *widget = constAs_Widget(i.object);
538             writeData_File(f, magicTabDocument_App_, 4);
539             int8_t flags = (document_Root(widget->root) == i.object ? current_DocumentStateFlag : 0);
540             if (widget->root == win->base.roots[1]) {
541                 flags |= rootIndex1_DocumentStateFlag;
542             }
543             write8_File(f, flags);
544             serializeState_DocumentWidget(i.object, stream_File(f));
545         }
546     }
547     else {
548         fprintf(stderr, "[App] failed to save state: %s\n", strerror(errno));
549     }
550     iRelease(f);
551 }
552 
553 #if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
checkAsleep_App_(uint32_t interval,void * param)554 static uint32_t checkAsleep_App_(uint32_t interval, void *param) {
555     iApp *d = param;
556     iUnused(d);
557     SDL_Event ev = { .type = SDL_USEREVENT };
558     ev.user.code = asleep_UserEventCode;
559     SDL_PushEvent(&ev);
560     return interval;
561 }
562 #endif
563 
postAutoReloadCommand_App_(uint32_t interval,void * param)564 static uint32_t postAutoReloadCommand_App_(uint32_t interval, void *param) {
565     iUnused(param);
566     postCommand_Root(NULL, "document.autoreload");
567     return interval;
568 }
569 
terminate_App_(int rc)570 static void terminate_App_(int rc) {
571     SDL_Quit();
572     deinit_Foundation();
573     exit(rc);
574 }
575 
576 #if defined (LAGRANGE_ENABLE_IPC)
communicateWithRunningInstance_App_(iApp * d,iProcessId instance,const iStringList * openCmds)577 static void communicateWithRunningInstance_App_(iApp *d, iProcessId instance,
578                                                 const iStringList *openCmds) {
579     iString *cmds = new_String();
580     iBool requestRaise = iFalse;
581     const iProcessId pid = currentId_Process();
582     iConstForEach(CommandLine, i, &d->args) {
583         if (i.argType == value_CommandLineArgType) {
584             continue;
585         }
586         if (equal_CommandLineConstIterator(&i, "go-home")) {
587             appendCStr_String(cmds, "navigate.home\n");
588             requestRaise = iTrue;
589         }
590         else if (equal_CommandLineConstIterator(&i, "new-tab")) {
591             iCommandLineArg *arg = argument_CommandLineConstIterator(&i);
592             if (!isEmpty_StringList(&arg->values)) {
593                 appendFormat_String(cmds, "open newtab:1 url:%s\n",
594                                     cstr_String(constAt_StringList(&arg->values, 0)));
595             }
596             else {
597                 appendCStr_String(cmds, "tabs.new\n");
598             }
599             iRelease(arg);
600             requestRaise = iTrue;
601         }
602         else if (equal_CommandLineConstIterator(&i, "close-tab")) {
603             appendCStr_String(cmds, "tabs.close\n");
604         }
605         else if (equal_CommandLineConstIterator(&i, "tab-url")) {
606             appendFormat_String(cmds, "ipc.active.url pid:%d\n", pid);
607         }
608         else if (equal_CommandLineConstIterator(&i, listTabUrls_CommandLineOption)) {
609             appendFormat_String(cmds, "ipc.list.urls pid:%d\n", pid);
610         }
611     }
612     if (!isEmpty_StringList(openCmds)) {
613         append_String(cmds, collect_String(joinCStr_StringList(openCmds, "\n")));
614         requestRaise = iTrue;
615     }
616     if (isEmpty_String(cmds)) {
617         /* By default open a new tab. */
618         appendCStr_String(cmds, "tabs.new\n");
619         requestRaise = iTrue;
620     }
621     if (!isEmpty_String(cmds)) {
622         iString *result = communicate_Ipc(cmds, requestRaise);
623         if (result) {
624             fwrite(cstr_String(result), 1, size_String(result), stdout);
625             fflush(stdout);
626         }
627         delete_String(result);
628     }
629     iUnused(instance);
630 //    else {
631 //        printf("Lagrange already running (PID %d)\n", instance);
632 //    }
633     terminate_App_(0);
634 }
635 #endif /* defined (LAGRANGE_ENABLE_IPC) */
636 
hasCommandLineOpenableScheme_(const iRangecc uri)637 static iBool hasCommandLineOpenableScheme_(const iRangecc uri) {
638     static const char *schemes[] = {
639         "gemini:", "gopher:", "finger:", "file:", "data:", "about:"
640     };
641     iForIndices(i, schemes) {
642         if (startsWithCase_Rangecc(uri, schemes[i])) {
643             return iTrue;
644         }
645     }
646     return iFalse;
647 }
648 
init_App_(iApp * d,int argc,char ** argv)649 static void init_App_(iApp *d, int argc, char **argv) {
650 #if defined (iPlatformLinux)
651     d->isRunningUnderWindowSystem = !iCmpStr(SDL_GetCurrentVideoDriver(), "x11") ||
652                                     !iCmpStr(SDL_GetCurrentVideoDriver(), "wayland");
653 #else
654     d->isRunningUnderWindowSystem = iTrue;
655 #endif
656     init_CommandLine(&d->args, argc, argv);
657     /* Where was the app started from? We ask SDL first because the command line alone is
658        not a reliable source of this information, particularly when it comes to different
659        operating systems. */ {
660         char *exec = SDL_GetBasePath();
661         if (exec) {
662             d->execPath = newCStr_String(concatPath_CStr(
663                 exec, cstr_Rangecc(baseName_Path(executablePath_CommandLine(&d->args)))));
664         }
665         else {
666             d->execPath = copy_String(executablePath_CommandLine(&d->args));
667         }
668         SDL_free(exec);
669     }
670 #if defined (iHaveLoadEmbed)
671     /* Load the resources from a file. */ {
672         if (!load_Embed(concatPath_CStr(cstr_String(execPath_App()), EMB_BIN))) {
673             if (!load_Embed(concatPath_CStr(cstr_String(execPath_App()), EMB_BIN2))) {
674                 if (!load_Embed("resources.lgr")) {
675                     fprintf(stderr, "failed to load resources: %s\n", strerror(errno));
676                     exit(-1);
677                 }
678             }
679         }
680     }
681 #endif
682     init_Lang();
683     /* Configure the valid command line options. */ {
684         defineValues_CommandLine(&d->args, "close-tab", 0);
685         defineValues_CommandLine(&d->args, "echo;E", 0);
686         defineValues_CommandLine(&d->args, "go-home", 0);
687         defineValues_CommandLine(&d->args, "help", 0);
688         defineValues_CommandLine(&d->args, listTabUrls_CommandLineOption, 0);
689         defineValues_CommandLine(&d->args, openUrlOrSearch_CommandLineOption, 1);
690         defineValues_CommandLine(&d->args, windowWidth_CommandLineOption, 1);
691         defineValues_CommandLine(&d->args, windowHeight_CommandLineOption, 1);
692         defineValuesN_CommandLine(&d->args, "new-tab", 0, 1);
693         defineValues_CommandLine(&d->args, "tab-url", 0);
694         defineValues_CommandLine(&d->args, "sw", 0);
695         defineValues_CommandLine(&d->args, "version;V", 0);
696     }
697     iStringList *openCmds = new_StringList();
698     /* Handle command line options. */ {
699         if (contains_CommandLine(&d->args, "help")) {
700             puts(cstr_Block(&blobArghelp_Embedded));
701             terminate_App_(0);
702         }
703         if (contains_CommandLine(&d->args, "version;V")) {
704             printf("Lagrange version " LAGRANGE_APP_VERSION "\n");
705             terminate_App_(0);
706         }
707         /* Check for URLs. */
708         iConstForEach(CommandLine, i, &d->args) {
709             const iRangecc arg = i.entry;
710             if (i.argType == value_CommandLineArgType) {
711                 /* URLs and file paths accepted. */
712                 const iBool isOpenable = hasCommandLineOpenableScheme_(arg);
713                 if (isOpenable || fileExistsCStr_FileInfo(cstr_Rangecc(arg))) {
714                     iString *decUrl =
715                         isOpenable ? urlDecodeExclude_String(collectNewRange_String(arg), "/?#:")
716                                    : makeFileUrl_String(collectNewRange_String(arg));
717                     pushBack_StringList(openCmds,
718                                         collectNewFormat_String(
719                                             "open newtab:1 url:%s", cstr_String(decUrl)));
720                     delete_String(decUrl);
721                 }
722                 else {
723                     fprintf(stderr, "Invalid URL/file: %s\n", cstr_Rangecc(arg));
724                     terminate_App_(1);
725                 }
726             }
727             else if (equal_CommandLineConstIterator(&i, openUrlOrSearch_CommandLineOption)) {
728                 const iCommandLineArg *arg = iClob(argument_CommandLineConstIterator(&i));
729                 const iString *input = value_CommandLineArg(arg, 0);
730                 if (startsWith_String(input, "//")) {
731                     input = collectNewFormat_String("gemini:%s", cstr_String(input));
732                 }
733                 if (hasCommandLineOpenableScheme_(range_String(input))) {
734                     input = collect_String(urlDecodeExclude_String(input, "/?#:"));
735                 }
736                 pushBack_StringList(
737                     openCmds,
738                     collectNewFormat_String("search newtab:1 query:%s", cstr_String(input)));
739             }
740             else if (!isDefined_CommandLine(&d->args, collectNewRange_String(i.entry))) {
741                 fprintf(stderr, "Unknown option: %s\n", cstr_Rangecc(arg));
742                 terminate_App_(1);
743             }
744         }
745     }
746 #if defined (LAGRANGE_ENABLE_IPC)
747     /* Only one instance is allowed to run at a time; the runtime files (bookmarks, etc.)
748        are not shareable. */ {
749         init_Ipc(dataDir_App_());
750         const iProcessId instance = check_Ipc();
751         if (instance) {
752             communicateWithRunningInstance_App_(d, instance, openCmds);
753             terminate_App_(0);
754         }
755         /* Some options are intended only for controlling other instances. */
756         if (contains_CommandLine(&d->args, listTabUrls_CommandLineOption)) {
757             terminate_App_(0);
758         }
759         listen_Ipc(); /* We'll respond to commands from other instances. */
760     }
761 #endif
762     printf("Lagrange: A Beautiful Gemini Client\n");
763     const iBool isFirstRun =
764         !fileExistsCStr_FileInfo(cleanedPath_CStr(concatPath_CStr(dataDir_App_(), "prefs.cfg")));
765     d->isFinishedLaunching = iFalse;
766     d->isLoadingPrefs      = iFalse;
767     d->warmupFrames        = 0;
768     d->launchCommands      = new_StringList();
769     iZap(d->lastDropTime);
770     init_SortedArray(&d->tickers, sizeof(iTicker), cmp_Ticker_);
771     d->lastTickerTime         = SDL_GetTicks();
772     d->elapsedSinceLastTicker = 0;
773     d->commandEcho            = checkArgument_CommandLine(&d->args, "echo;E") != NULL;
774     d->forceSoftwareRender    = checkArgument_CommandLine(&d->args, "sw") != NULL;
775     d->initialWindowRect      = init_Rect(-1, -1, 900, 560);
776 #if defined (iPlatformMsys)
777     /* Must scale by UI scaling factor. */
778     mulfv_I2(&d->initialWindowRect.size, desktopDPI_Win32());
779 #endif
780 #if defined (iPlatformLinux)
781     /* Scale by the primary (?) monitor DPI. */
782     if (isRunningUnderWindowSystem_App()) {
783         float vdpi;
784         SDL_GetDisplayDPI(0, NULL, NULL, &vdpi);
785         const float factor = vdpi / 96.0f;
786         mulfv_I2(&d->initialWindowRect.size, iMax(factor, 1.0f));
787     }
788 #endif
789     init_Prefs(&d->prefs);
790     init_SiteSpec(dataDir_App_());
791     setCStr_String(&d->prefs.downloadDir, downloadDir_App_());
792     set_Atomic(&d->pendingRefresh, iFalse);
793     d->isRunning = iFalse;
794     d->window    = NULL;
795     d->mimehooks = new_MimeHooks();
796     d->certs     = new_GmCerts(dataDir_App_());
797     d->visited   = new_Visited();
798     d->bookmarks = new_Bookmarks();
799     init_Periodic(&d->periodic);
800 #if defined (iPlatformAppleDesktop)
801     setupApplication_MacOS();
802 #endif
803 #if defined (iPlatformAppleMobile)
804     setupApplication_iOS();
805 #endif
806     init_Keys();
807     loadPalette_Color(dataDir_App_());
808     setThemePalette_Color(d->prefs.theme); /* default UI colors */
809     loadPrefs_App_(d);
810     load_Keys(dataDir_App_());
811     /* See if the user wants to override the window size. */ {
812         iCommandLineArg *arg = iClob(checkArgument_CommandLine(&d->args, windowWidth_CommandLineOption));
813         if (arg) {
814             d->initialWindowRect.size.x = toInt_String(value_CommandLineArg(arg, 0));
815         }
816         arg = iClob(checkArgument_CommandLine(&d->args, windowHeight_CommandLineOption));
817         if (arg) {
818             d->initialWindowRect.size.y = toInt_String(value_CommandLineArg(arg, 0));
819         }
820     }
821     init_PtrArray(&d->popupWindows);
822     d->window = new_MainWindow(d->initialWindowRect);
823     load_Visited(d->visited, dataDir_App_());
824     load_Bookmarks(d->bookmarks, dataDir_App_());
825     load_MimeHooks(d->mimehooks, dataDir_App_());
826     if (isFirstRun) {
827         /* Create the default bookmarks for a quick start. */
828         add_Bookmarks(d->bookmarks,
829                       collectNewCStr_String("gemini://skyjake.fi/lagrange/"),
830                       collectNewCStr_String("Lagrange"),
831                       NULL,
832                       0x1f306);
833         add_Bookmarks(d->bookmarks,
834                       collectNewCStr_String("gemini://skyjake.fi/lagrange/getting_started.gmi"),
835                       collectNewCStr_String("Getting Started"),
836                       NULL,
837                       0x1f306);
838     }
839     init_Feeds(dataDir_App_());
840     /* Widget state init. */
841     processEvents_App(postedEventsOnly_AppEventMode);
842     if (!loadState_App_(d)) {
843         postCommand_Root(NULL, "open url:about:help");
844     }
845     postCommand_Root(NULL, "~window.unfreeze");
846     postCommand_Root(NULL, "font.reset");
847     d->autoReloadTimer = SDL_AddTimer(60 * 1000, postAutoReloadCommand_App_, NULL);
848     postCommand_Root(NULL, "document.autoreload");
849 #if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
850     d->isIdling      = iFalse;
851     d->lastEventTime = 0;
852     d->sleepTimer    = SDL_AddTimer(1000, checkAsleep_App_, d);
853 #endif
854     d->isFinishedLaunching = iTrue;
855     /* Run any commands that were pending completion of launch. */ {
856         iForEach(StringList, i, d->launchCommands) {
857             postCommandString_Root(NULL, i.value);
858         }
859     }
860     /* URLs from the command line. */ {
861         iConstForEach(StringList, i, openCmds) {
862             postCommandString_Root(NULL, i.value);
863         }
864         iRelease(openCmds);
865     }
866     fetchRemote_Bookmarks(d->bookmarks);
867     if (deviceType_App() != desktop_AppDeviceType) {
868         /* HACK: Force a resize so widgets update their state. */
869         resize_MainWindow(d->window, -1, -1);
870     }
871 }
872 
deinit_App(iApp * d)873 static void deinit_App(iApp *d) {
874     iReverseForEach(PtrArray, i, &d->popupWindows) {
875         delete_Window(i.ptr);
876     }
877     iAssert(isEmpty_PtrArray(&d->popupWindows));
878     deinit_PtrArray(&d->popupWindows);
879 #if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
880     SDL_RemoveTimer(d->sleepTimer);
881 #endif
882     SDL_RemoveTimer(d->autoReloadTimer);
883     saveState_App_(d);
884     deinit_Feeds();
885     save_Keys(dataDir_App_());
886     deinit_Keys();
887     deinit_SiteSpec();
888     savePrefs_App_(d);
889     deinit_Prefs(&d->prefs);
890     save_Bookmarks(d->bookmarks, dataDir_App_());
891     delete_Bookmarks(d->bookmarks);
892     save_Visited(d->visited, dataDir_App_());
893     delete_Visited(d->visited);
894     delete_GmCerts(d->certs);
895     save_MimeHooks(d->mimehooks);
896     delete_MimeHooks(d->mimehooks);
897     delete_MainWindow(d->window);
898     d->window = NULL;
899     deinit_CommandLine(&d->args);
900     iRelease(d->launchCommands);
901     delete_String(d->execPath);
902 #if defined (LAGRANGE_ENABLE_IPC)
903     deinit_Ipc();
904 #endif
905     deinit_SortedArray(&d->tickers);
906     deinit_Periodic(&d->periodic);
907     deinit_Lang();
908     iRecycle();
909 }
910 
execPath_App(void)911 const iString *execPath_App(void) {
912     return app_.execPath;
913 }
914 
dataDir_App(void)915 const iString *dataDir_App(void) {
916     return collect_String(cleanedCStr_Path(dataDir_App_()));
917 }
918 
downloadDir_App(void)919 const iString *downloadDir_App(void) {
920     return collect_String(cleaned_Path(&app_.prefs.downloadDir));
921 }
922 
downloadPathForUrl_App(const iString * url,const iString * mime)923 const iString *downloadPathForUrl_App(const iString *url, const iString *mime) {
924     /* Figure out a file name from the URL. */
925     iUrl parts;
926     init_Url(&parts, url);
927     while (startsWith_Rangecc(parts.path, "/")) {
928         parts.path.start++;
929     }
930     while (endsWith_Rangecc(parts.path, "/")) {
931         parts.path.end--;
932     }
933     iString *name = collectNewCStr_String("pagecontent");
934     if (isEmpty_Range(&parts.path)) {
935         if (!isEmpty_Range(&parts.host)) {
936             setRange_String(name, parts.host);
937             replace_Block(&name->chars, '.', '_');
938         }
939     }
940     else {
941         const size_t slashPos = lastIndexOfCStr_Rangecc(parts.path, "/");
942         iRangecc fn = { parts.path.start + (slashPos != iInvalidPos ? slashPos + 1 : 0),
943                         parts.path.end };
944         if (!isEmpty_Range(&fn)) {
945             setRange_String(name, fn);
946         }
947     }
948     if (startsWith_String(name, "~")) {
949         /* This would be interpreted as a reference to a home directory. */
950         remove_Block(&name->chars, 0, 1);
951     }
952     iString *savePath = concat_Path(downloadDir_App(), name);
953     if (lastIndexOfCStr_String(savePath, ".") == iInvalidPos) {
954         /* No extension specified in URL. */
955         if (startsWith_String(mime, "text/gemini")) {
956             appendCStr_String(savePath, ".gmi");
957         }
958         else if (startsWith_String(mime, "text/")) {
959             appendCStr_String(savePath, ".txt");
960         }
961         else if (startsWith_String(mime, "image/")) {
962             appendCStr_String(savePath, cstr_String(mime) + 6);
963         }
964     }
965     if (fileExists_FileInfo(savePath)) {
966         /* Make it unique. */
967         iDate now;
968         initCurrent_Date(&now);
969         size_t insPos = lastIndexOfCStr_String(savePath, ".");
970         if (insPos == iInvalidPos) {
971             insPos = size_String(savePath);
972         }
973         const iString *date = collect_String(format_Date(&now, "_%Y-%m-%d_%H%M%S"));
974         insertData_Block(&savePath->chars, insPos, cstr_String(date), size_String(date));
975     }
976     return collect_String(savePath);
977 }
978 
debugInfo_App(void)979 const iString *debugInfo_App(void) {
980     extern char **environ; /* The environment variables. */
981     iApp *d = &app_;
982     iString *msg = collectNew_String();
983     iObjectList *docs = iClob(listDocuments_App(NULL));
984     format_String(msg, "# Debug information\n");
985     appendFormat_String(msg, "## Memory usage\n"); {
986         iMemInfo total = { 0, 0 };
987         iForEach(ObjectList, i, docs) {
988             iDocumentWidget *doc = i.object;
989             iMemInfo usage = memoryUsage_History(history_DocumentWidget(doc));
990             total.cacheSize += usage.cacheSize;
991             total.memorySize += usage.memorySize;
992         }
993         appendFormat_String(msg, "Total cache: %.3f MB\n", total.cacheSize / 1.0e6f);
994         appendFormat_String(msg, "Total memory: %.3f MB\n", total.memorySize / 1.0e6f);
995     }
996     appendFormat_String(msg, "## Documents\n");
997     iForEach(ObjectList, k, docs) {
998         iDocumentWidget *doc = k.object;
999         appendFormat_String(msg, "### Tab %d.%zu: %s\n",
1000                             constAs_Widget(doc)->root == get_Window()->roots[0] ? 1 : 2,
1001                             childIndex_Widget(constAs_Widget(doc)->parent, k.object) + 1,
1002                             cstr_String(bookmarkTitle_DocumentWidget(doc)));
1003         append_String(msg, collect_String(debugInfo_History(history_DocumentWidget(doc))));
1004     }
1005     appendCStr_String(msg, "## Environment\n```\n");
1006     for (char **env = environ; *env; env++) {
1007         appendFormat_String(msg, "%s\n", *env);
1008     }
1009     appendCStr_String(msg, "```\n");
1010     appendFormat_String(msg, "## Launch arguments\n```\n");
1011     iConstForEach(StringList, i, args_CommandLine(&d->args)) {
1012         appendFormat_String(msg, "%3zu : %s\n", i.pos, cstr_String(i.value));
1013     }
1014     appendFormat_String(msg, "```\n## Launch commands\n");
1015     iConstForEach(StringList, j, d->launchCommands) {
1016         appendFormat_String(msg, "%s\n", cstr_String(j.value));
1017     }
1018     appendFormat_String(msg, "## MIME hooks\n");
1019     append_String(msg, debugInfo_MimeHooks(d->mimehooks));
1020     return msg;
1021 }
1022 
clearCache_App_(void)1023 static void clearCache_App_(void) {
1024     iForEach(ObjectList, i, iClob(listDocuments_App(NULL))) {
1025         clearCache_History(history_DocumentWidget(i.object));
1026     }
1027 }
1028 
trimCache_App(void)1029 void trimCache_App(void) {
1030     iApp *d = &app_;
1031     size_t cacheSize = 0;
1032     const size_t limit = d->prefs.maxCacheSize * 1000000;
1033     iObjectList *docs = listDocuments_App(NULL);
1034     iForEach(ObjectList, i, docs) {
1035         cacheSize += cacheSize_History(history_DocumentWidget(i.object));
1036     }
1037     init_ObjectListIterator(&i, docs);
1038     iBool wasPruned = iFalse;
1039     while (cacheSize > limit) {
1040         iDocumentWidget *doc = i.object;
1041         const size_t pruned = pruneLeastImportant_History(history_DocumentWidget(doc));
1042         if (pruned) {
1043             cacheSize -= pruned;
1044             wasPruned = iTrue;
1045         }
1046         next_ObjectListIterator(&i);
1047         if (!i.value) {
1048             if (!wasPruned) break;
1049             wasPruned = iFalse;
1050             init_ObjectListIterator(&i, docs);
1051         }
1052     }
1053     iRelease(docs);
1054 }
1055 
trimMemory_App(void)1056 void trimMemory_App(void) {
1057     iApp *d = &app_;
1058     size_t memorySize = 0;
1059     const size_t limit = d->prefs.maxMemorySize * 1000000;
1060     iObjectList *docs = listDocuments_App(NULL);
1061     iForEach(ObjectList, i, docs) {
1062         memorySize += memorySize_History(history_DocumentWidget(i.object));
1063     }
1064     init_ObjectListIterator(&i, docs);
1065     iBool wasPruned = iFalse;
1066     while (memorySize > limit) {
1067         iDocumentWidget *doc = i.object;
1068         const size_t pruned = pruneLeastImportantMemory_History(history_DocumentWidget(doc));
1069         if (pruned) {
1070             memorySize -= pruned;
1071             wasPruned = iTrue;
1072         }
1073         next_ObjectListIterator(&i);
1074         if (!i.value) {
1075             if (!wasPruned) break;
1076             wasPruned = iFalse;
1077             init_ObjectListIterator(&i, docs);
1078         }
1079     }
1080     iRelease(docs);
1081 }
1082 
isWaitingAllowed_App_(iApp * d)1083 iLocalDef iBool isWaitingAllowed_App_(iApp *d) {
1084     if (!isEmpty_Periodic(&d->periodic)) {
1085         return iFalse;
1086     }
1087     if (d->warmupFrames > 0) {
1088         return iFalse;
1089     }
1090 #if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
1091     if (d->isIdling) {
1092         return iFalse;
1093     }
1094 #endif
1095     return !value_Atomic(&d->pendingRefresh) && isEmpty_SortedArray(&d->tickers);
1096 }
1097 
nextEvent_App_(iApp * d,enum iAppEventMode eventMode,SDL_Event * event)1098 static iBool nextEvent_App_(iApp *d, enum iAppEventMode eventMode, SDL_Event *event) {
1099     if (eventMode == waitForNewEvents_AppEventMode && isWaitingAllowed_App_(d)) {
1100         /* If there are periodic commands pending, wait only for a short while. */
1101         if (!isEmpty_Periodic(&d->periodic)) {
1102             return SDL_WaitEventTimeout(event, 500);
1103         }
1104         /* We may be allowed to block here until an event comes in. */
1105         if (isWaitingAllowed_App_(d)) {
1106             return SDL_WaitEvent(event);
1107         }
1108     }
1109     return SDL_PollEvent(event);
1110 }
1111 
listWindows_App_(const iApp * d)1112 static const iPtrArray *listWindows_App_(const iApp *d) {
1113     iPtrArray *list = collectNew_PtrArray();
1114     iReverseConstForEach(PtrArray, i, &d->popupWindows) {
1115         pushBack_PtrArray(list, i.ptr);
1116     }
1117     pushBack_PtrArray(list, d->window);
1118     return list;
1119 }
1120 
processEvents_App(enum iAppEventMode eventMode)1121 void processEvents_App(enum iAppEventMode eventMode) {
1122     iApp *d = &app_;
1123     iRoot *oldCurrentRoot = current_Root(); /* restored afterwards */
1124     SDL_Event ev;
1125     iBool gotEvents = iFalse;
1126     while (nextEvent_App_(d, eventMode, &ev)) {
1127 #if defined (iPlatformAppleMobile)
1128         if (processEvent_iOS(&ev)) {
1129             continue;
1130         }
1131 #endif
1132         switch (ev.type) {
1133             case SDL_QUIT:
1134                 d->isRunning = iFalse;
1135                 if (findWidget_App("prefs")) {
1136                     /* Make sure changed preferences get saved. */
1137                     postCommand_Root(NULL, "prefs.dismiss");
1138                     processEvents_App(postedEventsOnly_AppEventMode);
1139                 }
1140                 goto backToMainLoop;
1141             case SDL_APP_LOWMEMORY:
1142                 clearCache_App_();
1143                 break;
1144             case SDL_APP_WILLENTERFOREGROUND:
1145                 invalidate_Window(as_Window(d->window));
1146                 break;
1147             case SDL_APP_DIDENTERFOREGROUND:
1148                 gotEvents = iTrue;
1149                 d->warmupFrames = 5;
1150 #if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
1151                 d->isIdling = iFalse;
1152                 d->lastEventTime = SDL_GetTicks();
1153 #endif
1154                 postRefresh_App();
1155                 break;
1156             case SDL_APP_WILLENTERBACKGROUND:
1157 #if defined (iPlatformAppleMobile)
1158                 updateNowPlayingInfo_iOS();
1159 #endif
1160                 setFreezeDraw_MainWindow(d->window, iTrue);
1161                 savePrefs_App_(d);
1162                 saveState_App_(d);
1163                 break;
1164             case SDL_APP_TERMINATING:
1165                 setFreezeDraw_MainWindow(d->window, iTrue);
1166                 savePrefs_App_(d);
1167                 saveState_App_(d);
1168                 break;
1169             case SDL_DROPFILE: {
1170                 iBool wasUsed = processEvent_Window(as_Window(d->window), &ev);
1171                 if (!wasUsed) {
1172                     iBool newTab = iFalse;
1173                     if (elapsedSeconds_Time(&d->lastDropTime) < 0.1) {
1174                         /* Each additional drop gets a new tab. */
1175                         newTab = iTrue;
1176                     }
1177                     d->lastDropTime = now_Time();
1178                     if (startsWithCase_CStr(ev.drop.file, "gemini:") ||
1179                         startsWithCase_CStr(ev.drop.file, "gopher:") ||
1180                         startsWithCase_CStr(ev.drop.file, "file:")) {
1181                         postCommandf_Root(NULL, "~open newtab:%d url:%s", newTab, ev.drop.file);
1182                     }
1183                     else {
1184                         postCommandf_Root(NULL,
1185                             "~open newtab:%d url:%s", newTab, makeFileUrl_CStr(ev.drop.file));
1186                     }
1187                 }
1188                 break;
1189             }
1190             default: {
1191 #if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
1192                 if (ev.type == SDL_USEREVENT && ev.user.code == asleep_UserEventCode) {
1193                     if (SDL_GetTicks() - d->lastEventTime > idleThreshold_App_ &&
1194                         isEmpty_SortedArray(&d->tickers)) {
1195                         if (!d->isIdling) {
1196 //                            printf("[App] idling...\n");
1197 //                            fflush(stdout);
1198                         }
1199                         d->isIdling = iTrue;
1200                     }
1201                     continue;
1202                 }
1203                 d->lastEventTime = SDL_GetTicks();
1204                 if (d->isIdling) {
1205 //                    printf("[App] ...woke up\n");
1206 //                    fflush(stdout);
1207                 }
1208                 d->isIdling = iFalse;
1209 #endif
1210                 gotEvents = iTrue;
1211                 /* Keyboard modifier mapping. */
1212                 if (ev.type == SDL_KEYDOWN || ev.type == SDL_KEYUP) {
1213                     /* Track Caps Lock state as a modifier. */
1214                     if (ev.key.keysym.sym == SDLK_CAPSLOCK) {
1215                         setCapsLockDown_Keys(ev.key.state == SDL_PRESSED);
1216                     }
1217                     ev.key.keysym.mod = mapMods_Keys(ev.key.keysym.mod & ~KMOD_CAPS);
1218                 }
1219                 /* Scroll events may be per-pixel or mouse wheel steps. */
1220                 if (ev.type == SDL_MOUSEWHEEL) {
1221 #if defined (iPlatformAppleDesktop)
1222                     /* On macOS, we handle both trackpad and mouse events. We expect SDL to identify
1223                        which device is sending the event. */
1224                     if (ev.wheel.which == 0) {
1225                         /* Trackpad with precise scrolling w/inertia (points). */
1226                         setPerPixel_MouseWheelEvent(&ev.wheel, iTrue);
1227                         ev.wheel.x *= -d->window->base.pixelRatio;
1228                         ev.wheel.y *= d->window->base.pixelRatio;
1229                         /* Only scroll on one axis at a time. */
1230                         if (iAbs(ev.wheel.x) > iAbs(ev.wheel.y)) {
1231                             ev.wheel.y = 0;
1232                         }
1233                         else {
1234                             ev.wheel.x = 0;
1235                         }
1236                     }
1237                     else {
1238                         /* Disregard wheel acceleration applied by the OS. */
1239                         ev.wheel.x = -ev.wheel.x;
1240                         ev.wheel.y = iSign(ev.wheel.y);
1241                     }
1242 #endif
1243 #if defined (iPlatformMsys)
1244                     ev.wheel.x = -ev.wheel.x;
1245 #endif
1246                 }
1247 #if defined (LAGRANGE_ENABLE_MOUSE_TOUCH_EMULATION)
1248                 /* Convert mouse events to finger events to test the touch handling. */ {
1249                     static float xPrev = 0.0f;
1250                     static float yPrev = 0.0f;
1251                     if (ev.type == SDL_MOUSEBUTTONDOWN || ev.type == SDL_MOUSEBUTTONUP) {
1252                         const float xf = (d->window->pixelRatio * ev.button.x) / (float) d->window->size.x;
1253                         const float yf = (d->window->pixelRatio * ev.button.y) / (float) d->window->size.y;
1254                         ev.type = (ev.type == SDL_MOUSEBUTTONDOWN ? SDL_FINGERDOWN : SDL_FINGERUP);
1255                         ev.tfinger.x = xf;
1256                         ev.tfinger.y = yf;
1257                         ev.tfinger.dx = xf - xPrev;
1258                         ev.tfinger.dy = yf - yPrev;
1259                         xPrev = xf;
1260                         yPrev = yf;
1261                         ev.tfinger.fingerId = 0x1234;
1262                         ev.tfinger.pressure = 1.0f;
1263                         ev.tfinger.timestamp = SDL_GetTicks();
1264                         ev.tfinger.touchId = SDL_TOUCH_MOUSEID;
1265                     }
1266                     else if (ev.type == SDL_MOUSEMOTION) {
1267                         if (~ev.motion.state & SDL_BUTTON(SDL_BUTTON_LEFT)) {
1268                             continue; /* only when pressing a button */
1269                         }
1270                         const float xf = (d->window->pixelRatio * ev.motion.x) / (float) d->window->size.x;
1271                         const float yf = (d->window->pixelRatio * ev.motion.y) / (float) d->window->size.y;
1272                         ev.type = SDL_FINGERMOTION;
1273                         ev.tfinger.x = xf;
1274                         ev.tfinger.y = yf;
1275                         ev.tfinger.dx = xf - xPrev;
1276                         ev.tfinger.dy = yf - yPrev;
1277                         xPrev = xf;
1278                         yPrev = yf;
1279                         ev.tfinger.fingerId = 0x1234;
1280                         ev.tfinger.pressure = 1.0f;
1281                         ev.tfinger.timestamp = SDL_GetTicks();
1282                         ev.tfinger.touchId = SDL_TOUCH_MOUSEID;
1283                     }
1284                 }
1285 #endif
1286                 /* Per-window processing. */
1287                 iBool wasUsed = iFalse;
1288                 iConstForEach(PtrArray, iter, listWindows_App_(d)) {
1289                     iWindow *window = iter.ptr;
1290                     setCurrent_Window(window);
1291                     window->lastHover = window->hover;
1292                     wasUsed = processEvent_Window(window, &ev);
1293                     if (ev.type == SDL_MOUSEMOTION || ev.type == SDL_MOUSEBUTTONDOWN) {
1294                         break;
1295                     }
1296                     if (wasUsed) break;
1297                 }
1298                 setCurrent_Window(d->window);
1299                 if (!wasUsed) {
1300                     /* There may be a key binding for this. */
1301                     wasUsed = processEvent_Keys(&ev);
1302                 }
1303                 if (!wasUsed) {
1304                     /* Focus cycling. */
1305                     if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_TAB) {
1306                         setFocus_Widget(findFocusable_Widget(focus_Widget(),
1307                                                              ev.key.keysym.mod & KMOD_SHIFT
1308                                                                  ? backward_WidgetFocusDir
1309                                                                  : forward_WidgetFocusDir));
1310                         wasUsed = iTrue;
1311                     }
1312                 }
1313                 if (ev.type == SDL_USEREVENT && ev.user.code == command_UserEventCode) {
1314 #if defined (iPlatformAppleDesktop)
1315                     handleCommand_MacOS(command_UserEvent(&ev));
1316 #endif
1317 #if defined (iPlatformMsys)
1318                     handleCommand_Win32(command_UserEvent(&ev));
1319 #endif
1320                     if (isMetricsChange_UserEvent(&ev)) {
1321                         iConstForEach(PtrArray, iter, listWindows_App_(d)) {
1322                             iWindow *window = iter.ptr;
1323                             iForIndices(i, window->roots) {
1324                                 iRoot *root = window->roots[i];
1325                                 if (root) {
1326                                     arrange_Widget(root->widget);
1327                                 }
1328                             }
1329                         }
1330                     }
1331                     if (!wasUsed) {
1332                         /* No widget handled the command, so we'll do it. */
1333                         setCurrent_Window(d->window);
1334                         handleCommand_App(ev.user.data1);
1335                     }
1336                     /* Allocated by postCommand_Apps(). */
1337                     free(ev.user.data1);
1338                 }
1339                 /* Refresh after hover changes. */ {
1340                     iConstForEach(PtrArray, iter, listWindows_App_(d)) {
1341                         iWindow *window = iter.ptr;
1342                         if (window->lastHover != window->hover) {
1343                             refresh_Widget(window->lastHover);
1344                             refresh_Widget(window->hover);
1345                         }
1346                     }
1347                 }
1348                 break;
1349             }
1350         }
1351     }
1352 #if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
1353     if (d->isIdling && !gotEvents) {
1354         /* This is where we spend most of our time when idle. 60 Hz still quite a lot but we
1355            can't wait too long after the user tries to interact again with the app. In any
1356            case, on macOS SDL_WaitEvent() seems to use 10x more CPU time than sleeping. */
1357         SDL_Delay(1000 / 60);
1358     }
1359 #endif
1360 backToMainLoop:;
1361     setCurrent_Root(oldCurrentRoot);
1362 }
1363 
runTickers_App_(iApp * d)1364 static void runTickers_App_(iApp *d) {
1365     const uint32_t now = SDL_GetTicks();
1366     d->elapsedSinceLastTicker = (d->lastTickerTime ? now - d->lastTickerTime : 0);
1367     d->lastTickerTime = now;
1368     if (isEmpty_SortedArray(&d->tickers)) {
1369         d->lastTickerTime = 0;
1370         return;
1371     }
1372     /* Tickers may add themselves again, so we'll run off a copy. */
1373     iSortedArray *pending = copy_SortedArray(&d->tickers);
1374     clear_SortedArray(&d->tickers);
1375     postRefresh_App();
1376     iConstForEach(Array, i, &pending->values) {
1377         const iTicker *ticker = i.value;
1378         if (ticker->callback) {
1379             setCurrent_Root(ticker->root); /* root might be NULL */
1380             ticker->callback(ticker->context);
1381         }
1382     }
1383     setCurrent_Root(NULL);
1384     delete_SortedArray(pending);
1385     if (isEmpty_SortedArray(&d->tickers)) {
1386         d->lastTickerTime = 0;
1387     }
1388 }
1389 
resizeWatcher_(void * user,SDL_Event * event)1390 static int resizeWatcher_(void *user, SDL_Event *event) {
1391     iApp *d = user;
1392     if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
1393         const SDL_WindowEvent *winev = &event->window;
1394 #if defined (iPlatformMsys)
1395         resetFonts_Text(text_Window(d->window)); {
1396             SDL_Event u = { .type = SDL_USEREVENT };
1397             u.user.code = command_UserEventCode;
1398             u.user.data1 = strdup("theme.changed auto:1");
1399             dispatchEvent_Window(as_Window(d->window), &u);
1400         }
1401 #endif
1402         drawWhileResizing_MainWindow(d->window, winev->data1, winev->data2);
1403     }
1404     return 0;
1405 }
1406 
run_App_(iApp * d)1407 static int run_App_(iApp *d) {
1408     /* Initial arrangement. */
1409     iForIndices(i, d->window->base.roots) {
1410         if (d->window->base.roots[i]) {
1411             arrange_Widget(d->window->base.roots[i]->widget);
1412         }
1413     }
1414     d->isRunning = iTrue;
1415     SDL_EventState(SDL_DROPFILE, SDL_ENABLE); /* open files via drag'n'drop */
1416 #if defined (LAGRANGE_ENABLE_RESIZE_DRAW)
1417     SDL_AddEventWatch(resizeWatcher_, d); /* redraw window during resizing */
1418 #endif
1419     while (d->isRunning) {
1420         dispatchCommands_Periodic(&d->periodic);
1421         processEvents_App(waitForNewEvents_AppEventMode);
1422         runTickers_App_(d);
1423         refresh_App();
1424         /* Change the widget tree while we are not iterating through it. */
1425         checkPendingSplit_MainWindow(d->window);
1426         recycle_Garbage();
1427     }
1428     SDL_DelEventWatch(resizeWatcher_, d);
1429     return 0;
1430 }
1431 
refresh_App(void)1432 void refresh_App(void) {
1433     iApp *d = &app_;
1434 #if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
1435     if (d->warmupFrames == 0 && d->isIdling) {
1436         return;
1437     }
1438 #endif
1439     const iPtrArray *windows = listWindows_App_(d);
1440     /* Destroy pending widgets. */ {
1441         iConstForEach(PtrArray, j, windows) {
1442             iWindow *win = j.ptr;
1443             setCurrent_Window(win);
1444             iForIndices(i, win->roots) {
1445                 iRoot *root = win->roots[i];
1446                 if (root) {
1447                     destroyPending_Root(root);
1448                 }
1449             }
1450         }
1451     }
1452     /* TODO: Pending refresh is window-specific. */
1453     if (!exchange_Atomic(&d->pendingRefresh, iFalse)) {
1454         return;
1455     }
1456     /* Draw each window. */ {
1457         iConstForEach(PtrArray, j, windows) {
1458             iWindow *win = j.ptr;
1459             setCurrent_Window(win);
1460             switch (win->type) {
1461                 case main_WindowType:
1462     //                iTime draw;
1463     //                initCurrent_Time(&draw);
1464                     draw_MainWindow(as_MainWindow(win));
1465     //                printf("draw: %lld \u03bcs\n", (long long) (elapsedSeconds_Time(&draw) * 1000000));
1466     //                fflush(stdout);
1467                     break;
1468                 default:
1469                     draw_Window(win);
1470                     break;
1471             }
1472         }
1473     }
1474     if (d->warmupFrames > 0) {
1475         d->warmupFrames--;
1476     }
1477 }
1478 
isRefreshPending_App(void)1479 iBool isRefreshPending_App(void) {
1480     return value_Atomic(&app_.pendingRefresh);
1481 }
1482 
isFinishedLaunching_App(void)1483 iBool isFinishedLaunching_App(void) {
1484     return app_.isFinishedLaunching;
1485 }
1486 
elapsedSinceLastTicker_App(void)1487 uint32_t elapsedSinceLastTicker_App(void) {
1488     return app_.elapsedSinceLastTicker;
1489 }
1490 
prefs_App(void)1491 const iPrefs *prefs_App(void) {
1492     return &app_.prefs;
1493 }
1494 
forceSoftwareRender_App(void)1495 iBool forceSoftwareRender_App(void) {
1496     if (app_.forceSoftwareRender) {
1497         return iTrue;
1498     }
1499 #if defined (LAGRANGE_ENABLE_X11_SWRENDER)
1500     if (getenv("DISPLAY")) {
1501         return iTrue;
1502     }
1503 #endif
1504     return iFalse;
1505 }
1506 
setForceSoftwareRender_App(iBool sw)1507 void setForceSoftwareRender_App(iBool sw) {
1508     app_.forceSoftwareRender = sw;
1509 }
1510 
colorTheme_App(void)1511 enum iColorTheme colorTheme_App(void) {
1512     return app_.prefs.theme;
1513 }
1514 
schemeProxy_App(iRangecc scheme)1515 const iString *schemeProxy_App(iRangecc scheme) {
1516     iApp *d = &app_;
1517     const iString *proxy = NULL;
1518     if (equalCase_Rangecc(scheme, "gemini")) {
1519         proxy = &d->prefs.geminiProxy;
1520     }
1521     else if (equalCase_Rangecc(scheme, "gopher")) {
1522         proxy = &d->prefs.gopherProxy;
1523     }
1524     else if (equalCase_Rangecc(scheme, "http") || equalCase_Rangecc(scheme, "https")) {
1525         proxy = &d->prefs.httpProxy;
1526     }
1527     return isEmpty_String(proxy) ? NULL : proxy;
1528 }
1529 
run_App(int argc,char ** argv)1530 int run_App(int argc, char **argv) {
1531     init_App_(&app_, argc, argv);
1532     const int rc = run_App_(&app_);
1533     deinit_App(&app_);
1534     return rc;
1535 }
1536 
postRefresh_App(void)1537 void postRefresh_App(void) {
1538     iApp *d = &app_;
1539 #if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
1540     d->isIdling = iFalse;
1541 #endif
1542     const iBool wasPending = exchange_Atomic(&d->pendingRefresh, iTrue);
1543     if (!wasPending) {
1544         SDL_Event ev = { .type = SDL_USEREVENT };
1545         ev.user.code = refresh_UserEventCode;
1546         SDL_PushEvent(&ev);
1547     }
1548 }
1549 
postCommand_Root(iRoot * d,const char * command)1550 void postCommand_Root(iRoot *d, const char *command) {
1551     iAssert(command);
1552     if (strlen(command) == 0) {
1553         return;
1554     }
1555     if (*command == '!') {
1556         /* Global command; this is global context so just ignore. */
1557         command++;
1558     }
1559     if (*command == '~') {
1560         /* Requires launch to be finished; defer it if needed. */
1561         command++;
1562         if (!app_.isFinishedLaunching) {
1563             pushBackCStr_StringList(app_.launchCommands, command);
1564             return;
1565         }
1566     }
1567     SDL_Event ev = { .type = SDL_USEREVENT };
1568     ev.user.code = command_UserEventCode;
1569     /*ev.user.windowID = id_Window(get_Window());*/
1570     ev.user.data1 = strdup(command);
1571     ev.user.data2 = d; /* all events are root-specific */
1572     SDL_PushEvent(&ev);
1573     if (app_.commandEcho) {
1574         iWindow *win = get_Window();
1575         printf("%s[command] {%d} %s\n",
1576                app_.isLoadingPrefs ? "[Prefs] " : "",
1577                (d == NULL || win == NULL ? 0 : d == win->roots[0] ? 1 : 2),
1578                command); fflush(stdout);
1579     }
1580 }
1581 
postCommandf_Root(iRoot * d,const char * command,...)1582 void postCommandf_Root(iRoot *d, const char *command, ...) {
1583     iBlock chars;
1584     init_Block(&chars, 0);
1585     va_list args;
1586     va_start(args, command);
1587     vprintf_Block(&chars, command, args);
1588     va_end(args);
1589     postCommand_Root(d, cstr_Block(&chars));
1590     deinit_Block(&chars);
1591 }
1592 
postCommandf_App(const char * command,...)1593 void postCommandf_App(const char *command, ...) {
1594     iBlock chars;
1595     init_Block(&chars, 0);
1596     va_list args;
1597     va_start(args, command);
1598     vprintf_Block(&chars, command, args);
1599     va_end(args);
1600     postCommand_Root(NULL, cstr_Block(&chars));
1601     deinit_Block(&chars);
1602 }
1603 
rootOrder_App(iRoot * roots[2])1604 void rootOrder_App(iRoot *roots[2]) {
1605     const iWindow *win = get_Window();
1606     roots[0] = win->keyRoot;
1607     roots[1] = (roots[0] == win->roots[0] ? win->roots[1] : win->roots[0]);
1608 }
1609 
findWidget_App(const char * id)1610 iAny *findWidget_App(const char *id) {
1611     if (!*id) return NULL;
1612     iRoot *order[2];
1613     rootOrder_App(order);
1614     iForIndices(i, order) {
1615         if (order[i]) {
1616             iAny *found = findChild_Widget(order[i]->widget, id);
1617             if (found) {
1618                 return found;
1619             }
1620         }
1621     }
1622     return NULL;
1623 }
1624 
addTicker_App(iTickerFunc ticker,iAny * context)1625 void addTicker_App(iTickerFunc ticker, iAny *context) {
1626     iApp *d = &app_;
1627     insert_SortedArray(&d->tickers, &(iTicker){ context, get_Root(), ticker });
1628     postRefresh_App();
1629 }
1630 
addTickerRoot_App(iTickerFunc ticker,iRoot * root,iAny * context)1631 void addTickerRoot_App(iTickerFunc ticker, iRoot *root, iAny *context) {
1632     iApp *d = &app_;
1633     insert_SortedArray(&d->tickers, &(iTicker){ context, root, ticker });
1634     postRefresh_App();
1635 }
1636 
removeTicker_App(iTickerFunc ticker,iAny * context)1637 void removeTicker_App(iTickerFunc ticker, iAny *context) {
1638     iApp *d = &app_;
1639     remove_SortedArray(&d->tickers, &(iTicker){ context, NULL, ticker });
1640 }
1641 
addPopup_App(iWindow * popup)1642 void addPopup_App(iWindow *popup) {
1643     iApp *d = &app_;
1644     pushBack_PtrArray(&d->popupWindows, popup);
1645 }
1646 
removePopup_App(iWindow * popup)1647 void removePopup_App(iWindow *popup) {
1648     iApp *d = &app_;
1649     removeOne_PtrArray(&d->popupWindows, popup);
1650 }
1651 
mimeHooks_App(void)1652 iMimeHooks *mimeHooks_App(void) {
1653     return app_.mimehooks;
1654 }
1655 
periodic_App(void)1656 iPeriodic *periodic_App(void) {
1657     return &app_.periodic;
1658 }
1659 
isLandscape_App(void)1660 iBool isLandscape_App(void) {
1661     const iInt2 size = size_Window(get_Window());
1662     return size.x > size.y;
1663 }
1664 
deviceType_App(void)1665 enum iAppDeviceType deviceType_App(void) {
1666 #if defined (iPlatformMobilePhone)
1667     return phone_AppDeviceType;
1668 #elif defined (iPlatformMobileTablet)
1669     return tablet_AppDeviceType;
1670 #elif defined (iPlatformAppleMobile)
1671     return isPhone_iOS() ? phone_AppDeviceType : tablet_AppDeviceType;
1672 #else
1673     return desktop_AppDeviceType;
1674 #endif
1675 }
1676 
isRunningUnderWindowSystem_App(void)1677 iBool isRunningUnderWindowSystem_App(void) {
1678     return app_.isRunningUnderWindowSystem;
1679 }
1680 
certs_App(void)1681 iGmCerts *certs_App(void) {
1682     return app_.certs;
1683 }
1684 
visited_App(void)1685 iVisited *visited_App(void) {
1686     return app_.visited;
1687 }
1688 
bookmarks_App(void)1689 iBookmarks *bookmarks_App(void) {
1690     return app_.bookmarks;
1691 }
1692 
updatePrefsThemeButtons_(iWidget * d)1693 static void updatePrefsThemeButtons_(iWidget *d) {
1694     for (size_t i = 0; i < max_ColorTheme; i++) {
1695         setFlags_Widget(findChild_Widget(d, format_CStr("prefs.theme.%u", i)),
1696                         selected_WidgetFlag,
1697                         colorTheme_App() == i);
1698     }
1699     for (size_t i = 0; i < max_ColorAccent; i++) {
1700         setFlags_Widget(findChild_Widget(d, format_CStr("prefs.accent.%u", i)),
1701                         selected_WidgetFlag,
1702                         prefs_App()->accent == i);
1703     }
1704 }
1705 
updatePrefsPinSplitButtons_(iWidget * d,int value)1706 static void updatePrefsPinSplitButtons_(iWidget *d, int value) {
1707     for (int i = 0; i < 3; i++) {
1708         setFlags_Widget(findChild_Widget(d, format_CStr("prefs.pinsplit.%d", i)),
1709                         selected_WidgetFlag,
1710                         i == value);
1711     }
1712 }
1713 
updateScrollSpeedButtons_(iWidget * d,enum iScrollType type,const int value)1714 static void updateScrollSpeedButtons_(iWidget *d, enum iScrollType type, const int value) {
1715     const char *typeStr = (type == mouse_ScrollType ? "mouse" : "keyboard");
1716     for (int i = 0; i <= 40; i++) {
1717         setFlags_Widget(findChild_Widget(d, format_CStr("prefs.scrollspeed.%s.%d", typeStr, i)),
1718                         selected_WidgetFlag,
1719                         i == value);
1720     }
1721 }
1722 
updateColorThemeButton_(iLabelWidget * button,int theme)1723 static void updateColorThemeButton_(iLabelWidget *button, int theme) {
1724     /* TODO: These three functions are all the same? Cleanup? */
1725     if (!button) return;
1726     updateDropdownSelection_LabelWidget(button, format_CStr(".set arg:%d", theme));
1727 }
1728 
updateFontButton_(iLabelWidget * button,int font)1729 static void updateFontButton_(iLabelWidget *button, int font) {
1730     if (!button) return;
1731     updateDropdownSelection_LabelWidget(button, format_CStr(".set arg:%d", font));
1732 }
1733 
updateImageStyleButton_(iLabelWidget * button,int style)1734 static void updateImageStyleButton_(iLabelWidget *button, int style) {
1735     if (!button) return;
1736     updateDropdownSelection_LabelWidget(button, format_CStr(".set arg:%d", style));
1737 }
1738 
handlePrefsCommands_(iWidget * d,const char * cmd)1739 static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
1740     if (equal_Command(cmd, "prefs.dismiss") || equal_Command(cmd, "preferences")) {
1741         setupSheetTransition_Mobile(d, iFalse);
1742         setUiScale_Window(get_Window(),
1743                           toFloat_String(text_InputWidget(findChild_Widget(d, "prefs.uiscale"))));
1744 #if defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT)
1745         postCommandf_App("downloads path:%s",
1746                          cstr_String(text_InputWidget(findChild_Widget(d, "prefs.downloads"))));
1747 #endif
1748         postCommandf_App("customframe arg:%d",
1749                          isSelected_Widget(findChild_Widget(d, "prefs.customframe")));
1750         postCommandf_App("window.retain arg:%d",
1751                          isSelected_Widget(findChild_Widget(d, "prefs.retainwindow")));
1752         postCommandf_App("smoothscroll arg:%d",
1753                          isSelected_Widget(findChild_Widget(d, "prefs.smoothscroll")));
1754         postCommandf_App("imageloadscroll arg:%d",
1755                          isSelected_Widget(findChild_Widget(d, "prefs.imageloadscroll")));
1756         postCommandf_App("hidetoolbarscroll arg:%d",
1757                          isSelected_Widget(findChild_Widget(d, "prefs.hidetoolbarscroll")));
1758         postCommandf_App("ostheme arg:%d", isSelected_Widget(findChild_Widget(d, "prefs.ostheme")));
1759         postCommandf_App("font.user path:%s",
1760                          cstrText_InputWidget(findChild_Widget(d, "prefs.userfont")));
1761         postCommandf_App("decodeurls arg:%d",
1762                          isSelected_Widget(findChild_Widget(d, "prefs.decodeurls")));
1763         postCommandf_App("searchurl address:%s",
1764                          cstrText_InputWidget(findChild_Widget(d, "prefs.searchurl")));
1765         postCommandf_App("cachesize.set arg:%d",
1766                          toInt_String(text_InputWidget(findChild_Widget(d, "prefs.cachesize"))));
1767         postCommandf_App("memorysize.set arg:%d",
1768                          toInt_String(text_InputWidget(findChild_Widget(d, "prefs.memorysize"))));
1769         postCommandf_App("ca.file path:%s",
1770                          cstrText_InputWidget(findChild_Widget(d, "prefs.ca.file")));
1771         postCommandf_App("ca.path path:%s",
1772                          cstrText_InputWidget(findChild_Widget(d, "prefs.ca.path")));
1773         postCommandf_App("proxy.gemini address:%s",
1774                          cstrText_InputWidget(findChild_Widget(d, "prefs.proxy.gemini")));
1775         postCommandf_App("proxy.gopher address:%s",
1776                          cstrText_InputWidget(findChild_Widget(d, "prefs.proxy.gopher")));
1777         postCommandf_App("proxy.http address:%s",
1778                          cstrText_InputWidget(findChild_Widget(d, "prefs.proxy.http")));
1779         const iWidget *tabs = findChild_Widget(d, "prefs.tabs");
1780         if (tabs) {
1781             postCommandf_App("prefs.dialogtab arg:%u",
1782                              tabPageIndex_Widget(tabs, currentTabPage_Widget(tabs)));
1783         }
1784         destroy_Widget(d);
1785         postCommand_App("prefs.changed");
1786         return iTrue;
1787     }
1788     else if (equal_Command(cmd, "uilang")) {
1789         updateDropdownSelection_LabelWidget(findChild_Widget(d, "prefs.uilang"),
1790                                             cstr_String(string_Command(cmd, "id")));
1791         return iFalse;
1792     }
1793     else if (equal_Command(cmd, "quoteicon.set")) {
1794         const int arg = arg_Command(cmd);
1795         setFlags_Widget(findChild_Widget(d, "prefs.quoteicon.0"), selected_WidgetFlag, arg == 0);
1796         setFlags_Widget(findChild_Widget(d, "prefs.quoteicon.1"), selected_WidgetFlag, arg == 1);
1797         return iFalse;
1798     }
1799     else if (equal_Command(cmd, "returnkey.set")) {
1800         updateDropdownSelection_LabelWidget(findChild_Widget(d, "prefs.returnkey"),
1801                                             format_CStr("returnkey.set arg:%d", arg_Command(cmd)));
1802         return iFalse;
1803     }
1804     else if (equal_Command(cmd, "pinsplit.set")) {
1805         updatePrefsPinSplitButtons_(d, arg_Command(cmd));
1806         return iFalse;
1807     }
1808     else if (equal_Command(cmd, "scrollspeed")) {
1809         updateScrollSpeedButtons_(d, argLabel_Command(cmd, "type"), arg_Command(cmd));
1810         return iFalse;
1811     }
1812     else if (equal_Command(cmd, "doctheme.dark.set")) {
1813         updateColorThemeButton_(findChild_Widget(d, "prefs.doctheme.dark"), arg_Command(cmd));
1814         return iFalse;
1815     }
1816     else if (equal_Command(cmd, "doctheme.light.set")) {
1817         updateColorThemeButton_(findChild_Widget(d, "prefs.doctheme.light"), arg_Command(cmd));
1818         return iFalse;
1819     }
1820     else if (equal_Command(cmd, "imagestyle.set")) {
1821         updateImageStyleButton_(findChild_Widget(d, "prefs.imagestyle"), arg_Command(cmd));
1822         return iFalse;
1823     }
1824     else if (equal_Command(cmd, "font.set")) {
1825         updateFontButton_(findChild_Widget(d, "prefs.font"), arg_Command(cmd));
1826         return iFalse;
1827     }
1828     else if (equal_Command(cmd, "headingfont.set")) {
1829         updateFontButton_(findChild_Widget(d, "prefs.headingfont"), arg_Command(cmd));
1830         return iFalse;
1831     }
1832     else if (startsWith_CStr(cmd, "input.ended id:prefs.linespacing")) {
1833         /* Apply line spacing changes immediately. */
1834         const iInputWidget *lineSpacing = findWidget_App("prefs.linespacing");
1835         postCommandf_App("linespacing.set arg:%f", toFloat_String(text_InputWidget(lineSpacing)));
1836         return iTrue;
1837     }
1838     else if (equal_Command(cmd, "prefs.ostheme.changed")) {
1839         postCommandf_App("ostheme arg:%d", arg_Command(cmd));
1840     }
1841     else if (equal_Command(cmd, "theme.changed")) {
1842         updatePrefsThemeButtons_(d);
1843         if (!argLabel_Command(cmd, "auto")) {
1844             setToggle_Widget(findChild_Widget(d, "prefs.ostheme"), iFalse);
1845         }
1846     }
1847     else if (equalWidget_Command(cmd, d, "input.resized")) {
1848         updatePreferencesLayout_Widget(d);
1849         return iFalse;
1850     }
1851     return iFalse;
1852 }
1853 
document_Root(iRoot * d)1854 iDocumentWidget *document_Root(iRoot *d) {
1855     return iConstCast(iDocumentWidget *, currentTabPage_Widget(findChild_Widget(d->widget, "doctabs")));
1856 }
1857 
document_App(void)1858 iDocumentWidget *document_App(void) {
1859     return document_Root(get_Root());
1860 }
1861 
document_Command(const char * cmd)1862 iDocumentWidget *document_Command(const char *cmd) {
1863     /* Explicitly referenced. */
1864     iAnyObject *obj = pointerLabel_Command(cmd, "doc");
1865     if (obj) {
1866         return obj;
1867     }
1868     /* Implicit via source widget. */
1869     obj = pointer_Command(cmd);
1870     if (obj && isInstance_Object(obj, &Class_DocumentWidget)) {
1871         return obj;
1872     }
1873     /* Currently visible document. */
1874     return document_App();
1875 }
1876 
newTab_App(const iDocumentWidget * duplicateOf,iBool switchToNew)1877 iDocumentWidget *newTab_App(const iDocumentWidget *duplicateOf, iBool switchToNew) {
1878     //iApp *d = &app_;
1879     iWidget *tabs = findWidget_Root("doctabs");
1880     setFlags_Widget(tabs, hidden_WidgetFlag, iFalse);
1881     iWidget *newTabButton = findChild_Widget(tabs, "newtab");
1882     removeChild_Widget(newTabButton->parent, newTabButton);
1883     iDocumentWidget *doc;
1884     if (duplicateOf) {
1885         doc = duplicate_DocumentWidget(duplicateOf);
1886     }
1887     else {
1888         doc = new_DocumentWidget();
1889     }
1890     appendTabPage_Widget(tabs, as_Widget(doc), "", 0, 0);
1891     iRelease(doc); /* now owned by the tabs */
1892     addChild_Widget(findChild_Widget(tabs, "tabs.buttons"), iClob(newTabButton));
1893     if (switchToNew) {
1894         postCommandf_App("tabs.switch page:%p", doc);
1895     }
1896     arrange_Widget(tabs);
1897     refresh_Widget(tabs);
1898     postCommandf_Root(get_Root(), "tab.created id:%s", cstr_String(id_Widget(as_Widget(doc))));
1899     return doc;
1900 }
1901 
handleIdentityCreationCommands_(iWidget * dlg,const char * cmd)1902 static iBool handleIdentityCreationCommands_(iWidget *dlg, const char *cmd) {
1903     iApp *d = &app_;
1904     if (equal_Command(cmd, "ident.showmore")) {
1905         iForEach(ObjectList,
1906                  i,
1907                  children_Widget(findChild_Widget(
1908                      dlg, isUsingPanelLayout_Mobile() ? "panel.top" : "headings"))) {
1909             if (flags_Widget(i.object) & collapse_WidgetFlag) {
1910                 setFlags_Widget(i.object, hidden_WidgetFlag, iFalse);
1911             }
1912         }
1913         iForEach(ObjectList, j, children_Widget(findChild_Widget(dlg, "values"))) {
1914             if (flags_Widget(j.object) & collapse_WidgetFlag) {
1915                 setFlags_Widget(j.object, hidden_WidgetFlag, iFalse);
1916             }
1917         }
1918         setFlags_Widget(pointer_Command(cmd), disabled_WidgetFlag, iTrue);
1919         arrange_Widget(dlg);
1920         refresh_Widget(dlg);
1921         return iTrue;
1922     }
1923     if (equal_Command(cmd, "ident.scope")) {
1924         iLabelWidget *scope = findChild_Widget(dlg, "ident.scope");
1925         setText_LabelWidget(scope,
1926                             text_LabelWidget(child_Widget(
1927                                 findChild_Widget(as_Widget(scope), "menu"), arg_Command(cmd))));
1928         arrange_Widget(findWidget_App("ident"));
1929         return iTrue;
1930     }
1931     if (equal_Command(cmd, "ident.temp.changed")) {
1932         setFlags_Widget(
1933             findChild_Widget(dlg, "ident.temp.note"), hidden_WidgetFlag, !arg_Command(cmd));
1934         return iFalse;
1935     }
1936     if (equal_Command(cmd, "ident.accept") || equal_Command(cmd, "ident.cancel")) {
1937         if (equal_Command(cmd, "ident.accept")) {
1938             const iString *commonName   = text_InputWidget (findChild_Widget(dlg, "ident.common"));
1939             const iString *email        = text_InputWidget (findChild_Widget(dlg, "ident.email"));
1940             const iString *userId       = text_InputWidget (findChild_Widget(dlg, "ident.userid"));
1941             const iString *domain       = text_InputWidget (findChild_Widget(dlg, "ident.domain"));
1942             const iString *organization = text_InputWidget (findChild_Widget(dlg, "ident.org"));
1943             const iString *country      = text_InputWidget (findChild_Widget(dlg, "ident.country"));
1944             const iBool    isTemp       = isSelected_Widget(findChild_Widget(dlg, "ident.temp"));
1945             if (isEmpty_String(commonName)) {
1946                 makeSimpleMessage_Widget(orange_ColorEscape "${heading.newident.missing}",
1947                                          "${dlg.newindent.missing.commonname}");
1948                 return iTrue;
1949             }
1950             iDate until;
1951             /* Validate the date. */ {
1952                 iZap(until);
1953                 unsigned int val[6];
1954                 iDate today;
1955                 initCurrent_Date(&today);
1956                 const int n =
1957                     sscanf(cstr_String(text_InputWidget(findChild_Widget(dlg, "ident.until"))),
1958                            "%04u-%u-%u %u:%u:%u",
1959                            &val[0], &val[1], &val[2], &val[3], &val[4], &val[5]);
1960                 if (n <= 0) {
1961                     makeSimpleMessage_Widget(orange_ColorEscape "${heading.newident.date.bad}",
1962                                              "${dlg.newident.date.example}");
1963                     return iTrue;
1964                 }
1965                 until.year   = val[0];
1966                 until.month  = n >= 2 ? val[1] : 1;
1967                 until.day    = n >= 3 ? val[2] : 1;
1968                 until.hour   = n >= 4 ? val[3] : 0;
1969                 until.minute = n >= 5 ? val[4] : 0;
1970                 until.second = n == 6 ? val[5] : 0;
1971                 until.gmtOffsetSeconds = today.gmtOffsetSeconds;
1972                 /* In the past? */ {
1973                     iTime now, t;
1974                     initCurrent_Time(&now);
1975                     init_Time(&t, &until);
1976                     if (cmp_Time(&t, &now) <= 0) {
1977                         makeSimpleMessage_Widget(orange_ColorEscape "${heading.newident.date.bad}",
1978                                                  "${dlg.newident.date.past}");
1979                         return iTrue;
1980                     }
1981                 }
1982             }
1983             /* The input seems fine. */
1984             iGmIdentity *ident = newIdentity_GmCerts(d->certs,
1985                                                      isTemp ? temporary_GmIdentityFlag : 0,
1986                                                      until,
1987                                                      commonName,
1988                                                      email,
1989                                                      userId,
1990                                                      domain,
1991                                                      organization,
1992                                                      country);
1993             /* Use in the chosen scope. */ {
1994                 const iLabelWidget *scope    = findChild_Widget(dlg, "ident.scope");
1995                 const iString *     selLabel = text_LabelWidget(scope);
1996                 int                 selScope = 0;
1997                 iConstForEach(ObjectList,
1998                               i,
1999                               children_Widget(findChild_Widget(constAs_Widget(scope), "menu"))) {
2000                     if (isInstance_Object(i.object, &Class_LabelWidget)) {
2001                         const iLabelWidget *item = i.object;
2002                         if (equal_String(text_LabelWidget(item), selLabel)) {
2003                             break;
2004                         }
2005                         selScope++;
2006                     }
2007                 }
2008                 const iString *docUrl = url_DocumentWidget(document_Root(dlg->root));
2009                 iString *useUrl = NULL;
2010                 switch (selScope) {
2011                     case 0: /* current domain */
2012                         useUrl = collectNewFormat_String("gemini://%s",
2013                                                          cstr_Rangecc(urlHost_String(docUrl)));
2014                         break;
2015                     case 1: /* current page */
2016                         useUrl = collect_String(copy_String(docUrl));
2017                         break;
2018                     default: /* not used */
2019                         break;
2020                 }
2021                 if (useUrl) {
2022                     signIn_GmCerts(d->certs, ident, useUrl);
2023                     postCommand_App("navigate.reload");
2024                 }
2025             }
2026             postCommandf_App("sidebar.mode arg:%d show:1", identities_SidebarMode);
2027             postCommand_App("idents.changed");
2028         }
2029         setupSheetTransition_Mobile(dlg, iFalse);
2030         destroy_Widget(dlg);
2031         return iTrue;
2032     }
2033     return iFalse;
2034 }
2035 
willUseProxy_App(const iRangecc scheme)2036 iBool willUseProxy_App(const iRangecc scheme) {
2037     return schemeProxy_App(scheme) != NULL;
2038 }
2039 
searchQueryUrl_App(const iString * queryStringUnescaped)2040 const iString *searchQueryUrl_App(const iString *queryStringUnescaped) {
2041     iApp *d = &app_;
2042     if (isEmpty_String(&d->prefs.searchUrl)) {
2043         return collectNew_String();
2044     }
2045     const iString *escaped = urlEncode_String(queryStringUnescaped);
2046     return collectNewFormat_String("%s?%s", cstr_String(&d->prefs.searchUrl), cstr_String(escaped));
2047 }
2048 
resetFonts_App_(iApp * d)2049 static void resetFonts_App_(iApp *d) {
2050     iConstForEach(PtrArray, win, listWindows_App_(d)) {
2051         resetFonts_Text(text_Window(win.ptr));
2052     }
2053 }
2054 
handleCommand_App(const char * cmd)2055 iBool handleCommand_App(const char *cmd) {
2056     iApp *d = &app_;
2057     const iBool isFrozen = !d->window || d->window->isDrawFrozen;
2058     /* TODO: Maybe break this up a little bit? There's a very long list of ifs here. */
2059     if (equal_Command(cmd, "config.error")) {
2060         makeSimpleMessage_Widget(uiTextCaution_ColorEscape "CONFIG ERROR",
2061                                  format_CStr("Error in config file: %s\n"
2062                                              "See \"about:debug\" for details.",
2063                                              suffixPtr_Command(cmd, "where")));
2064         return iTrue;
2065     }
2066     else if (equal_Command(cmd, "prefs.changed")) {
2067         savePrefs_App_(d);
2068         return iTrue;
2069     }
2070     else if (equal_Command(cmd, "prefs.dialogtab")) {
2071         d->prefs.dialogTab = arg_Command(cmd);
2072         return iTrue;
2073     }
2074     else if (equal_Command(cmd, "uilang")) {
2075         const iString *lang = string_Command(cmd, "id");
2076         if (!equal_String(lang, &d->prefs.uiLanguage)) {
2077             set_String(&d->prefs.uiLanguage, lang);
2078             setCurrent_Lang(cstr_String(&d->prefs.uiLanguage));
2079             postCommand_App("lang.changed");
2080         }
2081         return iTrue;
2082     }
2083     else if (equal_Command(cmd, "translation.languages")) {
2084         d->prefs.langFrom = argLabel_Command(cmd, "from");
2085         d->prefs.langTo   = argLabel_Command(cmd, "to");
2086         return iTrue;
2087     }
2088     else if (equal_Command(cmd, "ui.split")) {
2089         if (argLabel_Command(cmd, "swap")) {
2090             swapRoots_MainWindow(d->window);
2091             return iTrue;
2092         }
2093         d->window->pendingSplitMode =
2094             (argLabel_Command(cmd, "axis") ? vertical_WindowSplit : 0) | (arg_Command(cmd) << 1);
2095         const char *url = suffixPtr_Command(cmd, "url");
2096         setCStr_String(d->window->pendingSplitUrl, url ? url : "");
2097         postRefresh_App();
2098         return iTrue;
2099     }
2100     else if (equal_Command(cmd, "window.retain")) {
2101         d->prefs.retainWindowSize = arg_Command(cmd);
2102         return iTrue;
2103     }
2104     else if (equal_Command(cmd, "customframe")) {
2105         d->prefs.customFrame = arg_Command(cmd);
2106         return iTrue;
2107     }
2108     else if (equal_Command(cmd, "window.maximize")) {
2109         if (!argLabel_Command(cmd, "toggle")) {
2110             setSnap_MainWindow(d->window, maximized_WindowSnap);
2111         }
2112         else {
2113             setSnap_MainWindow(d->window, snap_MainWindow(d->window) == maximized_WindowSnap ? 0 :
2114                            maximized_WindowSnap);
2115         }
2116         return iTrue;
2117     }
2118     else if (equal_Command(cmd, "window.fullscreen")) {
2119         const iBool wasFull = snap_MainWindow(d->window) == fullscreen_WindowSnap;
2120         setSnap_MainWindow(d->window, wasFull ? 0 : fullscreen_WindowSnap);
2121         postCommandf_App("window.fullscreen.changed arg:%d", !wasFull);
2122         return iTrue;
2123     }
2124     else if (equal_Command(cmd, "font.reset")) {
2125         resetFonts_App_(d);
2126         return iTrue;
2127     }
2128     else if (equal_Command(cmd, "font.user")) {
2129         const char *path = suffixPtr_Command(cmd, "path");
2130         if (cmp_String(&d->prefs.symbolFontPath, path)) {
2131             if (!isFrozen) {
2132                 setFreezeDraw_MainWindow(get_MainWindow(), iTrue);
2133             }
2134             setCStr_String(&d->prefs.symbolFontPath, path);
2135             loadUserFonts_Text();
2136             resetFonts_App_(d);
2137             if (!isFrozen) {
2138                 postCommand_App("font.changed");
2139                 postCommand_App("window.unfreeze");
2140             }
2141         }
2142         return iTrue;
2143     }
2144     else if (equal_Command(cmd, "font.set")) {
2145         if (!isFrozen) {
2146             setFreezeDraw_MainWindow(get_MainWindow(), iTrue);
2147         }
2148         d->prefs.font = arg_Command(cmd);
2149         setContentFont_Text(text_Window(d->window), d->prefs.font);
2150         if (!isFrozen) {
2151             postCommand_App("font.changed");
2152             postCommand_App("window.unfreeze");
2153         }
2154         return iTrue;
2155     }
2156     else if (equal_Command(cmd, "headingfont.set")) {
2157         if (!isFrozen) {
2158             setFreezeDraw_MainWindow(get_MainWindow(), iTrue);
2159         }
2160         d->prefs.headingFont = arg_Command(cmd);
2161         setHeadingFont_Text(text_Window(d->window), d->prefs.headingFont);
2162         if (!isFrozen) {
2163             postCommand_App("font.changed");
2164             postCommand_App("window.unfreeze");
2165         }
2166         return iTrue;
2167     }
2168     else if (equal_Command(cmd, "zoom.set")) {
2169         if (!isFrozen) {
2170             setFreezeDraw_MainWindow(get_MainWindow(), iTrue); /* no intermediate draws before docs updated */
2171         }
2172         d->prefs.zoomPercent = arg_Command(cmd);
2173         setContentFontSize_Text(text_Window(d->window), (float) d->prefs.zoomPercent / 100.0f);
2174         if (!isFrozen) {
2175             postCommand_App("font.changed");
2176             postCommand_App("window.unfreeze");
2177         }
2178         return iTrue;
2179     }
2180     else if (equal_Command(cmd, "zoom.delta")) {
2181         if (!isFrozen) {
2182             setFreezeDraw_MainWindow(get_MainWindow(), iTrue); /* no intermediate draws before docs updated */
2183         }
2184         int delta = arg_Command(cmd);
2185         if (d->prefs.zoomPercent < 100 || (delta < 0 && d->prefs.zoomPercent == 100)) {
2186             delta /= 2;
2187         }
2188         d->prefs.zoomPercent = iClamp(d->prefs.zoomPercent + delta, 50, 200);
2189         setContentFontSize_Text(text_Window(d->window), (float) d->prefs.zoomPercent / 100.0f);
2190         if (!isFrozen) {
2191             postCommand_App("font.changed");
2192             postCommand_App("window.unfreeze");
2193         }
2194         return iTrue;
2195     }
2196     else if (equal_Command(cmd, "smoothscroll")) {
2197         d->prefs.smoothScrolling = arg_Command(cmd);
2198         return iTrue;
2199     }
2200     else if (equal_Command(cmd, "scrollspeed")) {
2201         const int type = argLabel_Command(cmd, "type");
2202         if (type == keyboard_ScrollType || type == mouse_ScrollType) {
2203             d->prefs.smoothScrollSpeed[type] = iClamp(arg_Command(cmd), 1, 40);
2204         }
2205         return iTrue;
2206     }
2207     else if (equal_Command(cmd, "decodeurls")) {
2208         d->prefs.decodeUserVisibleURLs = arg_Command(cmd);
2209         return iTrue;
2210     }
2211     else if (equal_Command(cmd, "imageloadscroll")) {
2212         d->prefs.loadImageInsteadOfScrolling = arg_Command(cmd);
2213         return iTrue;
2214     }
2215     else if (equal_Command(cmd, "hidetoolbarscroll")) {
2216         d->prefs.hideToolbarOnScroll = arg_Command(cmd);
2217         if (!d->prefs.hideToolbarOnScroll) {
2218             showToolbar_Root(get_Root(), iTrue);
2219         }
2220         return iTrue;
2221     }
2222     else if (equal_Command(cmd, "returnkey.set")) {
2223         d->prefs.returnKey = arg_Command(cmd);
2224         return iTrue;
2225     }
2226     else if (equal_Command(cmd, "pinsplit.set")) {
2227         d->prefs.pinSplit = arg_Command(cmd);
2228         return iTrue;
2229     }
2230     else if (equal_Command(cmd, "theme.set")) {
2231         const int isAuto = argLabel_Command(cmd, "auto");
2232         d->prefs.theme = arg_Command(cmd);
2233         if (!isAuto) {
2234             postCommand_App("ostheme arg:0");
2235         }
2236         setThemePalette_Color(d->prefs.theme);
2237         postCommandf_App("theme.changed auto:%d", isAuto);
2238         return iTrue;
2239     }
2240     else if (equal_Command(cmd, "accent.set")) {
2241         d->prefs.accent = arg_Command(cmd);
2242         setThemePalette_Color(d->prefs.theme);
2243         if (!isFrozen) {
2244             invalidate_Window(d->window);
2245         }
2246         return iTrue;
2247     }
2248     else if (equal_Command(cmd, "ostheme")) {
2249         d->prefs.useSystemTheme = arg_Command(cmd);
2250         return iTrue;
2251     }
2252     else if (equal_Command(cmd, "doctheme.dark.set")) {
2253         d->prefs.docThemeDark = arg_Command(cmd);
2254         if (!isFrozen) {
2255             invalidate_Window(d->window);
2256         }
2257         return iTrue;
2258     }
2259     else if (equal_Command(cmd, "doctheme.light.set")) {
2260         d->prefs.docThemeLight = arg_Command(cmd);
2261         if (!isFrozen) {
2262             invalidate_Window(d->window);
2263         }
2264         return iTrue;
2265     }
2266     else if (equal_Command(cmd, "imagestyle.set")) {
2267         d->prefs.imageStyle = arg_Command(cmd);
2268         return iTrue;
2269     }
2270     else if (equal_Command(cmd, "linewidth.set")) {
2271         d->prefs.lineWidth = iMax(20, arg_Command(cmd));
2272         postCommand_App("document.layout.changed");
2273         return iTrue;
2274     }
2275     else if (equal_Command(cmd, "linespacing.set")) {
2276         d->prefs.lineSpacing = iMax(0.5f, argf_Command(cmd));
2277         postCommand_App("document.layout.changed redo:1");
2278         return iTrue;
2279     }
2280     else if (equal_Command(cmd, "quoteicon.set")) {
2281         d->prefs.quoteIcon = arg_Command(cmd) != 0;
2282         postCommand_App("document.layout.changed");
2283         return iTrue;
2284     }
2285     else if (equal_Command(cmd, "prefs.mono.gemini.changed") ||
2286              equal_Command(cmd, "prefs.mono.gopher.changed")) {
2287         const iBool isSet = (arg_Command(cmd) != 0);
2288         if (!isFrozen) {
2289             setFreezeDraw_MainWindow(get_MainWindow(), iTrue);
2290         }
2291         if (startsWith_CStr(cmd, "prefs.mono.gemini")) {
2292             d->prefs.monospaceGemini = isSet;
2293         }
2294         else {
2295             d->prefs.monospaceGopher = isSet;
2296         }
2297         if (!isFrozen) {
2298             //resetFonts_Text(); /* clear the glyph cache */
2299             postCommand_App("font.changed");
2300             postCommand_App("window.unfreeze");
2301         }
2302         return iTrue;
2303     }
2304     else if (equal_Command(cmd, "prefs.boldlink.dark.changed") ||
2305              equal_Command(cmd, "prefs.boldlink.light.changed")) {
2306         const iBool isSet = (arg_Command(cmd) != 0);
2307         if (startsWith_CStr(cmd, "prefs.boldlink.dark")) {
2308             d->prefs.boldLinkDark = isSet;
2309         }
2310         else {
2311             d->prefs.boldLinkLight = isSet;
2312         }
2313         if (!d->isLoadingPrefs) {
2314             postCommand_App("font.changed");
2315         }
2316         return iTrue;
2317     }
2318     else if (equal_Command(cmd, "prefs.biglede.changed")) {
2319         d->prefs.bigFirstParagraph = arg_Command(cmd) != 0;
2320         if (!d->isLoadingPrefs) {
2321             postCommand_App("document.layout.changed");
2322         }
2323         return iTrue;
2324     }
2325     else if (equal_Command(cmd, "prefs.plaintext.wrap.changed")) {
2326         d->prefs.plainTextWrap = arg_Command(cmd) != 0;
2327         if (!d->isLoadingPrefs) {
2328             postCommand_App("document.layout.changed");
2329         }
2330         return iTrue;
2331     }
2332     else if (equal_Command(cmd, "prefs.sideicon.changed")) {
2333         d->prefs.sideIcon = arg_Command(cmd) != 0;
2334         postRefresh_App();
2335         return iTrue;
2336     }
2337     else if (equal_Command(cmd, "prefs.centershort.changed")) {
2338         d->prefs.centerShortDocs = arg_Command(cmd) != 0;
2339         if (!isFrozen) {
2340             invalidate_Window(d->window);
2341         }
2342         return iTrue;
2343     }
2344     else if (equal_Command(cmd, "prefs.collapsepreonload.changed")) {
2345         d->prefs.collapsePreOnLoad = arg_Command(cmd) != 0;
2346         return iTrue;
2347     }
2348     else if (equal_Command(cmd, "prefs.hoverlink.changed")) {
2349         d->prefs.hoverLink = arg_Command(cmd) != 0;
2350         postRefresh_App();
2351         return iTrue;
2352     }
2353     else if (equal_Command(cmd, "prefs.hoverlink.toggle")) {
2354         d->prefs.hoverLink = !d->prefs.hoverLink;
2355         postRefresh_App();
2356         return iTrue;
2357     }
2358     else if (equal_Command(cmd, "prefs.archive.openindex.changed")) {
2359         d->prefs.openArchiveIndexPages = arg_Command(cmd) != 0;
2360         return iTrue;
2361     }
2362     else if (equal_Command(cmd, "prefs.bookmarks.addbottom.changed")) {
2363         d->prefs.addBookmarksToBottom = arg_Command(cmd) != 0;
2364         return iTrue;
2365     }
2366     else if (equal_Command(cmd, "prefs.animate.changed")) {
2367         d->prefs.uiAnimations = arg_Command(cmd) != 0;
2368         return iTrue;
2369     }
2370     else if (equal_Command(cmd, "saturation.set")) {
2371         d->prefs.saturation = (float) arg_Command(cmd) / 100.0f;
2372         if (!isFrozen) {
2373             invalidate_Window(d->window);
2374         }
2375         return iTrue;
2376     }
2377     else if (equal_Command(cmd, "cachesize.set")) {
2378         d->prefs.maxCacheSize = arg_Command(cmd);
2379         if (d->prefs.maxCacheSize <= 0) {
2380             d->prefs.maxCacheSize = 0;
2381         }
2382         return iTrue;
2383     }
2384     else if (equal_Command(cmd, "memorysize.set")) {
2385         d->prefs.maxMemorySize = arg_Command(cmd);
2386         if (d->prefs.maxMemorySize <= 0) {
2387             d->prefs.maxMemorySize = 0;
2388         }
2389         return iTrue;
2390     }
2391     else if (equal_Command(cmd, "searchurl")) {
2392         iString *url = &d->prefs.searchUrl;
2393         setCStr_String(url, suffixPtr_Command(cmd, "address"));
2394         if (startsWith_String(url, "//")) {
2395             prependCStr_String(url, "gemini:");
2396         }
2397         if (!isEmpty_String(url) && !startsWithCase_String(url, "gemini://")) {
2398             prependCStr_String(url, "gemini://");
2399         }
2400         return iTrue;
2401     }
2402     else if (equal_Command(cmd, "proxy.gemini")) {
2403         setCStr_String(&d->prefs.geminiProxy, suffixPtr_Command(cmd, "address"));
2404         return iTrue;
2405     }
2406     else if (equal_Command(cmd, "proxy.gopher")) {
2407         setCStr_String(&d->prefs.gopherProxy, suffixPtr_Command(cmd, "address"));
2408         return iTrue;
2409     }
2410     else if (equal_Command(cmd, "proxy.http")) {
2411         setCStr_String(&d->prefs.httpProxy, suffixPtr_Command(cmd, "address"));
2412         return iTrue;
2413     }
2414 #if defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT)
2415     else if (equal_Command(cmd, "downloads")) {
2416         setCStr_String(&d->prefs.downloadDir, suffixPtr_Command(cmd, "path"));
2417         return iTrue;
2418     }
2419 #endif
2420     else if (equal_Command(cmd, "downloads.open")) {
2421         postCommandf_App("open url:%s", cstrCollect_String(makeFileUrl_String(downloadDir_App())));
2422         return iTrue;
2423     }
2424     else if (equal_Command(cmd, "ca.file")) {
2425         setCStr_String(&d->prefs.caFile, suffixPtr_Command(cmd, "path"));
2426         if (!argLabel_Command(cmd, "noset")) {
2427             setCACertificates_TlsRequest(&d->prefs.caFile, &d->prefs.caPath);
2428         }
2429         return iTrue;
2430     }
2431     else if (equal_Command(cmd, "ca.path")) {
2432         setCStr_String(&d->prefs.caPath, suffixPtr_Command(cmd, "path"));
2433         if (!argLabel_Command(cmd, "noset")) {
2434             setCACertificates_TlsRequest(&d->prefs.caFile, &d->prefs.caPath);
2435         }
2436         return iTrue;
2437     }
2438     else if (equal_Command(cmd, "search")) {
2439         const int newTab = argLabel_Command(cmd, "newtab");
2440         const iString *query = collect_String(suffix_Command(cmd, "query"));
2441         if (!isLikelyUrl_String(query)) {
2442             const iString *url = searchQueryUrl_App(query);
2443             if (!isEmpty_String(url)) {
2444                 postCommandf_App("open newtab:%d url:%s", newTab, cstr_String(url));
2445             }
2446         }
2447         else {
2448             postCommandf_App("open newtab:%d url:%s", newTab, cstr_String(query));
2449         }
2450         return iTrue;
2451     }
2452     else if (equal_Command(cmd, "open")) {
2453         iString *url = collectNewCStr_String(suffixPtr_Command(cmd, "url"));
2454         const iBool noProxy     = argLabel_Command(cmd, "noproxy") != 0;
2455         const iBool fromSidebar = argLabel_Command(cmd, "fromsidebar") != 0;
2456         iUrl parts;
2457         init_Url(&parts, url);
2458         if (equalCase_Rangecc(parts.scheme, "titan")) {
2459             iUploadWidget *upload = new_UploadWidget();
2460             setUrl_UploadWidget(upload, url);
2461             setResponseViewer_UploadWidget(upload, document_App());
2462             addChild_Widget(get_Root()->widget, iClob(upload));
2463 //            finalizeSheet_Mobile(as_Widget(upload));
2464             setupSheetTransition_Mobile(as_Widget(upload), iTrue);
2465             postRefresh_App();
2466             return iTrue;
2467         }
2468         if (argLabel_Command(cmd, "default") || equalCase_Rangecc(parts.scheme, "mailto") ||
2469             ((noProxy || isEmpty_String(&d->prefs.httpProxy)) &&
2470              (equalCase_Rangecc(parts.scheme, "http") ||
2471               equalCase_Rangecc(parts.scheme, "https")))) {
2472             openInDefaultBrowser_App(url);
2473             return iTrue;
2474         }
2475         const int newTab = argLabel_Command(cmd, "newtab");
2476         if (newTab & otherRoot_OpenTabFlag && numRoots_Window(get_Window()) == 1) {
2477             /* Need to split first. */
2478             const iInt2 winSize = get_Window()->size;
2479             postCommandf_App("ui.split arg:3 axis:%d newtab:%d url:%s",
2480                              (float) winSize.x / (float) winSize.y < 0.7f ? 1 : 0,
2481                              newTab & ~otherRoot_OpenTabFlag,
2482                              cstr_String(url));
2483             return iTrue;
2484         }
2485         iRoot *root = get_Root();
2486         iRoot *oldRoot = root;
2487         if (newTab & otherRoot_OpenTabFlag) {
2488             root = otherRoot_Window(as_Window(d->window), root);
2489             setKeyRoot_Window(as_Window(d->window), root);
2490             setCurrent_Root(root); /* need to change for widget creation */
2491         }
2492         iDocumentWidget *doc = document_Command(cmd);
2493         if (newTab & (new_OpenTabFlag | newBackground_OpenTabFlag)) {
2494             doc = newTab_App(NULL, (newTab & new_OpenTabFlag) != 0); /* `newtab:2` to open in background */
2495         }
2496         iHistory *history = history_DocumentWidget(doc);
2497         const iBool isHistory = argLabel_Command(cmd, "history") != 0;
2498         int redirectCount = argLabel_Command(cmd, "redirect");
2499         if (!isHistory) {
2500             if (redirectCount) {
2501                 replace_History(history, url);
2502             }
2503             else {
2504                 add_History(history, url);
2505             }
2506         }
2507         setInitialScroll_DocumentWidget(doc, argfLabel_Command(cmd, "scroll"));
2508         setRedirectCount_DocumentWidget(doc, redirectCount);
2509         showCollapsed_Widget(findWidget_App("document.progress"), iFalse);
2510         if (prefs_App()->decodeUserVisibleURLs) {
2511             urlDecodePath_String(url);
2512         }
2513         else {
2514             urlEncodePath_String(url);
2515         }
2516         setUrlFlags_DocumentWidget(doc, url,
2517            (isHistory   ? useCachedContentIfAvailable_DocumentWidgetSetUrlFlag : 0) |
2518            (fromSidebar ? openedFromSidebar_DocumentWidgetSetUrlFlag : 0));
2519         /* Optionally, jump to a text in the document. This will only work if the document
2520            is already available, e.g., it's from "about:" or restored from cache. */
2521         const iRangecc gotoHeading = range_Command(cmd, "gotoheading");
2522         if (gotoHeading.start) {
2523             postCommandf_Root(root, "document.goto heading:%s", cstr_Rangecc(gotoHeading));
2524         }
2525         const iRangecc gotoUrlHeading = range_Command(cmd, "gotourlheading");
2526         if (gotoUrlHeading.start) {
2527             postCommandf_Root(root, "document.goto heading:%s",
2528                              cstrCollect_String(urlDecode_String(
2529                                  collect_String(newRange_String(gotoUrlHeading)))));
2530         }
2531         setCurrent_Root(oldRoot);
2532     }
2533     else if (equal_Command(cmd, "file.open")) {
2534         const char *path = suffixPtr_Command(cmd, "path");
2535         if (path) {
2536             postCommandf_App("open temp:%d url:%s",
2537                              argLabel_Command(cmd, "temp"),
2538                              makeFileUrl_CStr(path));
2539             return iTrue;
2540         }
2541 #if defined (iPlatformAppleMobile)
2542         pickFileForOpening_iOS();
2543 #endif
2544         return iTrue;
2545     }
2546     else if (equal_Command(cmd, "file.delete")) {
2547         const char *path = suffixPtr_Command(cmd, "path");
2548         if (argLabel_Command(cmd, "confirm")) {
2549             makeQuestion_Widget(
2550                 uiHeading_ColorEscape "${heading.file.delete}",
2551                 format_CStr("${dlg.file.delete.confirm}\n%s", path),
2552                 (iMenuItem[]){
2553                     { "${cancel}", 0, 0, NULL },
2554                     { uiTextCaution_ColorEscape "${dlg.file.delete}", 0, 0,
2555                       format_CStr("!file.delete path:%s", path) } },
2556                 2);
2557         }
2558         else {
2559             remove(path);
2560         }
2561         return iTrue;
2562     }
2563     else if (equal_Command(cmd, "document.request.cancelled")) {
2564         /* TODO: How should cancelled requests be treated in the history? */
2565 #if 0
2566         if (d->historyPos == 0) {
2567             iHistoryItem *item = historyItem_App_(d, 0);
2568             if (item) {
2569                 /* Pop this cancelled URL off history. */
2570                 deinit_HistoryItem(item);
2571                 popBack_Array(&d->history);
2572                 printHistory_App_(d);
2573             }
2574         }
2575 #endif
2576         return iFalse;
2577     }
2578     else if (equal_Command(cmd, "tabs.new")) {
2579         const iBool isDuplicate = argLabel_Command(cmd, "duplicate") != 0;
2580         newTab_App(isDuplicate ? document_App() : NULL, iTrue);
2581         if (!isDuplicate) {
2582             postCommand_App("navigate.home focus:1");
2583         }
2584         return iTrue;
2585     }
2586     else if (equal_Command(cmd, "tabs.close")) {
2587         iWidget *tabs = findWidget_App("doctabs");
2588 #if defined (iPlatformMobile)
2589         /* Can't close the last on mobile. */
2590         if (tabCount_Widget(tabs) == 1 && numRoots_Window(get_Window()) == 1) {
2591             postCommand_App("navigate.home");
2592             return iTrue;
2593         }
2594 #endif
2595         const iRangecc tabId = range_Command(cmd, "id");
2596         iWidget *      doc   = !isEmpty_Range(&tabId) ? findWidget_App(cstr_Rangecc(tabId))
2597                                                       : document_App();
2598         iBool  wasCurrent = (doc == (iWidget *) document_App());
2599         size_t index      = tabPageIndex_Widget(tabs, doc);
2600         iBool  wasClosed  = iFalse;
2601         postCommand_App("document.openurls.changed");
2602         if (argLabel_Command(cmd, "toright")) {
2603             while (tabCount_Widget(tabs) > index + 1) {
2604                 destroy_Widget(removeTabPage_Widget(tabs, index + 1));
2605             }
2606             wasClosed = iTrue;
2607         }
2608         if (argLabel_Command(cmd, "toleft")) {
2609             while (index-- > 0) {
2610                 destroy_Widget(removeTabPage_Widget(tabs, 0));
2611             }
2612             postCommandf_App("tabs.switch page:%p", tabPage_Widget(tabs, 0));
2613             wasClosed = iTrue;
2614         }
2615         if (wasClosed) {
2616             arrange_Widget(tabs);
2617             return iTrue;
2618         }
2619         const iBool isSplit = numRoots_Window(get_Window()) > 1;
2620         if (tabCount_Widget(tabs) > 1 || isSplit) {
2621             iWidget *closed = removeTabPage_Widget(tabs, index);
2622             destroy_Widget(closed); /* released later */
2623             if (index == tabCount_Widget(tabs)) {
2624                 index--;
2625             }
2626             if (tabCount_Widget(tabs) == 0) {
2627                 iAssert(isSplit);
2628                 postCommand_App("ui.split arg:0");
2629             }
2630             else {
2631                 arrange_Widget(tabs);
2632                 if (wasCurrent) {
2633                     postCommandf_App("tabs.switch page:%p", tabPage_Widget(tabs, index));
2634                 }
2635             }
2636         }
2637         else {
2638             postCommand_App("quit");
2639         }
2640         return iTrue;
2641     }
2642     else if (equal_Command(cmd, "keyroot.next")) {
2643         if (setKeyRoot_Window(as_Window(d->window),
2644                               otherRoot_Window(as_Window(d->window), d->window->base.keyRoot))) {
2645             setFocus_Widget(NULL);
2646         }
2647         return iTrue;
2648     }
2649     else if (equal_Command(cmd, "quit")) {
2650         SDL_Event ev;
2651         ev.type = SDL_QUIT;
2652         SDL_PushEvent(&ev);
2653     }
2654     else if (equal_Command(cmd, "preferences")) {
2655         iWidget *dlg = makePreferences_Widget();
2656         updatePrefsThemeButtons_(dlg);
2657         setText_InputWidget(findChild_Widget(dlg, "prefs.downloads"), &d->prefs.downloadDir);
2658         setToggle_Widget(findChild_Widget(dlg, "prefs.hoverlink"), d->prefs.hoverLink);
2659         setToggle_Widget(findChild_Widget(dlg, "prefs.smoothscroll"), d->prefs.smoothScrolling);
2660         setToggle_Widget(findChild_Widget(dlg, "prefs.imageloadscroll"), d->prefs.loadImageInsteadOfScrolling);
2661         setToggle_Widget(findChild_Widget(dlg, "prefs.hidetoolbarscroll"), d->prefs.hideToolbarOnScroll);
2662         setToggle_Widget(findChild_Widget(dlg, "prefs.bookmarks.addbottom"), d->prefs.addBookmarksToBottom);
2663         setToggle_Widget(findChild_Widget(dlg, "prefs.archive.openindex"), d->prefs.openArchiveIndexPages);
2664         setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->prefs.useSystemTheme);
2665         setToggle_Widget(findChild_Widget(dlg, "prefs.customframe"), d->prefs.customFrame);
2666         setToggle_Widget(findChild_Widget(dlg, "prefs.animate"), d->prefs.uiAnimations);
2667         setText_InputWidget(findChild_Widget(dlg, "prefs.userfont"), &d->prefs.symbolFontPath);
2668         updatePrefsPinSplitButtons_(dlg, d->prefs.pinSplit);
2669         updateScrollSpeedButtons_(dlg, mouse_ScrollType, d->prefs.smoothScrollSpeed[mouse_ScrollType]);
2670         updateScrollSpeedButtons_(dlg, keyboard_ScrollType, d->prefs.smoothScrollSpeed[keyboard_ScrollType]);
2671         updateDropdownSelection_LabelWidget(findChild_Widget(dlg, "prefs.uilang"), cstr_String(&d->prefs.uiLanguage));
2672         updateDropdownSelection_LabelWidget(
2673             findChild_Widget(dlg, "prefs.returnkey"),
2674             format_CStr("returnkey.set arg:%d", d->prefs.returnKey));
2675         setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->prefs.retainWindowSize);
2676         setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"),
2677                             collectNewFormat_String("%g", uiScale_Window(as_Window(d->window))));
2678         setFlags_Widget(findChild_Widget(dlg, format_CStr("prefs.font.%d", d->prefs.font)),
2679                         selected_WidgetFlag,
2680                         iTrue);
2681         setFlags_Widget(
2682             findChild_Widget(dlg, format_CStr("prefs.headingfont.%d", d->prefs.headingFont)),
2683             selected_WidgetFlag,
2684             iTrue);
2685         setFlags_Widget(findChild_Widget(dlg, "prefs.mono.gemini"),
2686                         selected_WidgetFlag,
2687                         d->prefs.monospaceGemini);
2688         setFlags_Widget(findChild_Widget(dlg, "prefs.mono.gopher"),
2689                         selected_WidgetFlag,
2690                         d->prefs.monospaceGopher);
2691         setFlags_Widget(findChild_Widget(dlg, "prefs.boldlink.dark"),
2692                         selected_WidgetFlag,
2693                         d->prefs.boldLinkDark);
2694         setFlags_Widget(findChild_Widget(dlg, "prefs.boldlink.light"),
2695                         selected_WidgetFlag,
2696                         d->prefs.boldLinkLight);
2697         setFlags_Widget(
2698             findChild_Widget(dlg, format_CStr("prefs.linewidth.%d", d->prefs.lineWidth)),
2699             selected_WidgetFlag,
2700             iTrue);
2701         setText_InputWidget(findChild_Widget(dlg, "prefs.linespacing"),
2702                             collectNewFormat_String("%.2f", d->prefs.lineSpacing));
2703         setFlags_Widget(
2704             findChild_Widget(dlg, format_CStr("prefs.quoteicon.%d", d->prefs.quoteIcon)),
2705             selected_WidgetFlag,
2706             iTrue);
2707         setToggle_Widget(findChild_Widget(dlg, "prefs.biglede"), d->prefs.bigFirstParagraph);
2708         setToggle_Widget(findChild_Widget(dlg, "prefs.plaintext.wrap"), d->prefs.plainTextWrap);
2709         setToggle_Widget(findChild_Widget(dlg, "prefs.sideicon"), d->prefs.sideIcon);
2710         setToggle_Widget(findChild_Widget(dlg, "prefs.centershort"), d->prefs.centerShortDocs);
2711         setToggle_Widget(findChild_Widget(dlg, "prefs.collapsepreonload"), d->prefs.collapsePreOnLoad);
2712         updateColorThemeButton_(findChild_Widget(dlg, "prefs.doctheme.dark"), d->prefs.docThemeDark);
2713         updateColorThemeButton_(findChild_Widget(dlg, "prefs.doctheme.light"), d->prefs.docThemeLight);
2714         updateImageStyleButton_(findChild_Widget(dlg, "prefs.imagestyle"), d->prefs.imageStyle);
2715         updateFontButton_(findChild_Widget(dlg, "prefs.font"), d->prefs.font);
2716         updateFontButton_(findChild_Widget(dlg, "prefs.headingfont"), d->prefs.headingFont);
2717         setFlags_Widget(
2718             findChild_Widget(
2719                 dlg, format_CStr("prefs.saturation.%d", (int) (d->prefs.saturation * 3.99f))),
2720             selected_WidgetFlag,
2721             iTrue);
2722         setText_InputWidget(findChild_Widget(dlg, "prefs.cachesize"),
2723                             collectNewFormat_String("%d", d->prefs.maxCacheSize));
2724         setText_InputWidget(findChild_Widget(dlg, "prefs.memorysize"),
2725                             collectNewFormat_String("%d", d->prefs.maxMemorySize));
2726         setToggle_Widget(findChild_Widget(dlg, "prefs.decodeurls"), d->prefs.decodeUserVisibleURLs);
2727         setText_InputWidget(findChild_Widget(dlg, "prefs.searchurl"), &d->prefs.searchUrl);
2728         setText_InputWidget(findChild_Widget(dlg, "prefs.ca.file"), &d->prefs.caFile);
2729         setText_InputWidget(findChild_Widget(dlg, "prefs.ca.path"), &d->prefs.caPath);
2730         setText_InputWidget(findChild_Widget(dlg, "prefs.proxy.gemini"), &d->prefs.geminiProxy);
2731         setText_InputWidget(findChild_Widget(dlg, "prefs.proxy.gopher"), &d->prefs.gopherProxy);
2732         setText_InputWidget(findChild_Widget(dlg, "prefs.proxy.http"), &d->prefs.httpProxy);
2733         iWidget *tabs = findChild_Widget(dlg, "prefs.tabs");
2734         if (tabs) {
2735             showTabPage_Widget(tabs, tabPage_Widget(tabs, d->prefs.dialogTab));
2736         }
2737         setCommandHandler_Widget(dlg, handlePrefsCommands_);
2738     }
2739     else if (equal_Command(cmd, "navigate.home")) {
2740         /* Look for bookmarks tagged "homepage". */
2741         iRegExp *pattern = iClob(new_RegExp("\\b" homepage_BookmarkTag "\\b",
2742                                             caseInsensitive_RegExpOption));
2743         const iPtrArray *homepages =
2744             list_Bookmarks(d->bookmarks, NULL, filterTagsRegExp_Bookmarks, pattern);
2745         if (isEmpty_PtrArray(homepages)) {
2746             postCommand_Root(get_Root(), "open url:about:lagrange");
2747         }
2748         else {
2749             iStringSet *urls = iClob(new_StringSet());
2750             iConstForEach(PtrArray, i, homepages) {
2751                 const iBookmark *bm = i.ptr;
2752                 /* Try to switch to a different bookmark. */
2753                 if (cmpStringCase_String(url_DocumentWidget(document_App()), &bm->url)) {
2754                     insert_StringSet(urls, &bm->url);
2755                 }
2756             }
2757             if (!isEmpty_StringSet(urls)) {
2758                 postCommandf_Root(get_Root(),
2759                     "open url:%s",
2760                     cstr_String(constAt_StringSet(urls, iRandoms(0, size_StringSet(urls)))));
2761             }
2762         }
2763         if (argLabel_Command(cmd, "focus")) {
2764             postCommand_Root(get_Root(), "navigate.focus");
2765         }
2766         return iTrue;
2767     }
2768     else if (equal_Command(cmd, "bookmark.add")) {
2769         iDocumentWidget *doc = document_App();
2770         if (suffixPtr_Command(cmd, "url")) {
2771             iString *title = collect_String(newRange_String(range_Command(cmd, "title")));
2772             replace_String(title, "%20", " ");
2773             makeBookmarkCreation_Widget(collect_String(suffix_Command(cmd, "url")),
2774                                         title,
2775                                         0x1f588 /* pin */);
2776         }
2777         else {
2778             makeBookmarkCreation_Widget(url_DocumentWidget(doc),
2779                                         bookmarkTitle_DocumentWidget(doc),
2780                                         siteIcon_GmDocument(document_DocumentWidget(doc)));
2781         }
2782         if (deviceType_App() == desktop_AppDeviceType) {
2783             postCommand_App("focus.set id:bmed.title");
2784         }
2785         return iTrue;
2786     }
2787     else if (equal_Command(cmd, "feeds.subscribe")) {
2788         const iString *url = url_DocumentWidget(document_App());
2789         if (isEmpty_String(url)) {
2790             return iTrue;
2791         }
2792         makeFeedSettings_Widget(findUrl_Bookmarks(d->bookmarks, url));
2793         return iTrue;
2794     }
2795     else if (equal_Command(cmd, "bookmarks.addfolder")) {
2796         const int parentId = argLabel_Command(cmd, "parent");
2797         if (suffixPtr_Command(cmd, "value")) {
2798             uint32_t id = add_Bookmarks(d->bookmarks, NULL,
2799                                         collect_String(suffix_Command(cmd, "value")), NULL, 0);
2800             if (parentId) {
2801                 get_Bookmarks(d->bookmarks, id)->parentId = parentId;
2802             }
2803             postCommandf_App("bookmarks.changed added:%zu", id);
2804         }
2805         else {
2806             iWidget *dlg = makeValueInput_Widget(
2807                 get_Root()->widget, collectNewCStr_String(cstr_Lang("dlg.addfolder.defaulttitle")),
2808                 uiHeading_ColorEscape "${heading.addfolder}", "${dlg.addfolder.prompt}",
2809                 uiTextAction_ColorEscape "${dlg.addfolder}",
2810                 format_CStr("bookmarks.addfolder parent:%d", parentId));
2811             setSelectAllOnFocus_InputWidget(findChild_Widget(dlg, "input"), iTrue);
2812         }
2813         return iTrue;
2814     }
2815     else if (equal_Command(cmd, "bookmarks.sort")) {
2816         sort_Bookmarks(d->bookmarks, arg_Command(cmd), cmpTitleAscending_Bookmark);
2817         postCommand_App("bookmarks.changed");
2818         return iTrue;
2819     }
2820     else if (equal_Command(cmd, "bookmarks.reload.remote")) {
2821         fetchRemote_Bookmarks(bookmarks_App());
2822         return iTrue;
2823     }
2824     else if (equal_Command(cmd, "bookmarks.request.finished")) {
2825         requestFinished_Bookmarks(bookmarks_App(), pointerLabel_Command(cmd, "req"));
2826         return iTrue;
2827     }
2828     else if (equal_Command(cmd, "bookmarks.changed")) {
2829         save_Bookmarks(d->bookmarks, dataDir_App_());
2830         return iFalse;
2831     }
2832     else if (equal_Command(cmd, "feeds.refresh")) {
2833         refresh_Feeds();
2834         return iTrue;
2835     }
2836     else if (startsWith_CStr(cmd, "feeds.update.")) {
2837         const iWidget *navBar = findChild_Widget(get_Window()->roots[0]->widget, "navbar");
2838         iAnyObject *prog = findChild_Widget(navBar, "feeds.progress");
2839         if (equal_Command(cmd, "feeds.update.started") ||
2840             equal_Command(cmd, "feeds.update.progress")) {
2841             const int num   = arg_Command(cmd);
2842             const int total = argLabel_Command(cmd, "total");
2843             updateTextAndResizeWidthCStr_LabelWidget(prog,
2844                                                      flags_Widget(navBar) & tight_WidgetFlag ||
2845                                                              deviceType_App() == phone_AppDeviceType
2846                                                          ? star_Icon
2847                                                          : star_Icon " ${status.feeds}");
2848             showCollapsed_Widget(prog, iTrue);
2849             setFixedSize_Widget(findChild_Widget(prog, "feeds.progressbar"),
2850                                 init_I2(total ? width_Widget(prog) * num / total : 0, -1));
2851         }
2852         else if (equal_Command(cmd, "feeds.update.finished")) {
2853             showCollapsed_Widget(prog, iFalse);
2854             refreshFinished_Feeds();
2855             refresh_Widget(findWidget_App("url"));
2856             return iFalse;
2857         }
2858         return iFalse;
2859     }
2860     else if (equal_Command(cmd, "visited.changed")) {
2861         save_Visited(d->visited, dataDir_App_());
2862         return iFalse;
2863     }
2864     else if (equal_Command(cmd, "document.changed")) {
2865         /* Set of open tabs has changed. */
2866         postCommand_App("document.openurls.changed");
2867         if (deviceType_App() == phone_AppDeviceType) {
2868             showToolbar_Root(d->window->base.roots[0], iTrue);
2869         }
2870         return iFalse;
2871     }
2872     else if (equal_Command(cmd, "ident.new")) {
2873         iWidget *dlg = makeIdentityCreation_Widget();
2874         setFocus_Widget(findChild_Widget(dlg, "ident.until"));
2875         setCommandHandler_Widget(dlg, handleIdentityCreationCommands_);
2876         return iTrue;
2877     }
2878     else if (equal_Command(cmd, "ident.import")) {
2879         iCertImportWidget *imp = new_CertImportWidget();
2880         setPageContent_CertImportWidget(imp, sourceContent_DocumentWidget(document_App()));
2881         addChild_Widget(get_Root()->widget, iClob(imp));
2882 //        finalizeSheet_Mobile(as_Widget(imp));
2883         arrange_Widget(as_Widget(imp));
2884         setupSheetTransition_Mobile(as_Widget(imp), iTrue);
2885         postRefresh_App();
2886         return iTrue;
2887     }
2888     else if (equal_Command(cmd, "ident.signin")) {
2889         const iString *url = collect_String(suffix_Command(cmd, "url"));
2890         signIn_GmCerts(
2891             d->certs,
2892             findIdentity_GmCerts(d->certs, collect_Block(hexDecode_Rangecc(range_Command(cmd, "ident")))),
2893             url);
2894         postCommand_App("idents.changed");
2895         return iTrue;
2896     }
2897     else if (equal_Command(cmd, "ident.signout")) {
2898         iGmIdentity *ident = findIdentity_GmCerts(
2899             d->certs, collect_Block(hexDecode_Rangecc(range_Command(cmd, "ident"))));
2900         if (arg_Command(cmd)) {
2901             clearUse_GmIdentity(ident);
2902         }
2903         else {
2904             setUse_GmIdentity(ident, collect_String(suffix_Command(cmd, "url")), iFalse);
2905         }
2906         postCommand_App("idents.changed");
2907         return iTrue;
2908     }
2909     else if (equal_Command(cmd, "idents.changed")) {
2910         saveIdentities_GmCerts(d->certs);
2911         return iFalse;
2912     }
2913     else if (equal_Command(cmd, "os.theme.changed")) {
2914         if (d->prefs.useSystemTheme) {
2915             const int dark     = argLabel_Command(cmd, "dark");
2916             const int contrast = argLabel_Command(cmd, "contrast");
2917             postCommandf_App("theme.set arg:%d auto:1",
2918                              dark ? (contrast ? pureBlack_ColorTheme : dark_ColorTheme)
2919                                   : (contrast ? pureWhite_ColorTheme : light_ColorTheme));
2920         }
2921         return iFalse;
2922     }
2923 #if defined (LAGRANGE_ENABLE_IPC)
2924     else if (equal_Command(cmd, "ipc.list.urls")) {
2925         iProcessId pid = argLabel_Command(cmd, "pid");
2926         if (pid) {
2927             iString *urls = collectNew_String();
2928             iConstForEach(ObjectList, i, iClob(listDocuments_App(NULL))) {
2929                 append_String(urls, url_DocumentWidget(i.object));
2930                 appendCStr_String(urls, "\n");
2931             }
2932             write_Ipc(pid, urls, response_IpcWrite);
2933         }
2934         return iTrue;
2935     }
2936     else if (equal_Command(cmd, "ipc.active.url")) {
2937         write_Ipc(argLabel_Command(cmd, "pid"),
2938                   collectNewFormat_String("%s\n", cstr_String(url_DocumentWidget(document_App()))),
2939                   response_IpcWrite);
2940         return iTrue;
2941     }
2942     else if (equal_Command(cmd, "ipc.signal")) {
2943         if (argLabel_Command(cmd, "raise")) {
2944             if (d->window && d->window->base.win) {
2945                 SDL_RaiseWindow(d->window->base.win);
2946             }
2947         }
2948         signal_Ipc(arg_Command(cmd));
2949         return iTrue;
2950     }
2951 #endif /* defined (LAGRANGE_ENABLE_IPC) */
2952     else {
2953         return iFalse;
2954     }
2955     return iTrue;
2956 }
2957 
openInDefaultBrowser_App(const iString * url)2958 void openInDefaultBrowser_App(const iString *url) {
2959 #if SDL_VERSION_ATLEAST(2, 0, 14)
2960     if (SDL_OpenURL(cstr_String(url)) == 0) {
2961         return;
2962     }
2963 #endif
2964 #if !defined (iPlatformAppleMobile)
2965     iProcess *proc = new_Process();
2966     setArguments_Process(proc,
2967 #if defined (iPlatformAppleDesktop)
2968                          iClob(newStringsCStr_StringList("/usr/bin/env", "open", cstr_String(url), NULL))
2969 #elif defined (iPlatformLinux) || defined (iPlatformOther) || defined (iPlatformHaiku)
2970                          iClob(newStringsCStr_StringList("/usr/bin/env", "xdg-open", cstr_String(url), NULL))
2971 #elif defined (iPlatformMsys)
2972         iClob(newStringsCStr_StringList(
2973             concatPath_CStr(cstr_String(execPath_App()), "../urlopen.bat"),
2974             cstr_String(url),
2975             NULL))
2976         /* TODO: The prompt window is shown momentarily... */
2977 #endif
2978     );
2979     start_Process(proc);
2980     waitForFinished_Process(proc); /* TODO: test on Windows */
2981     iRelease(proc);
2982 #endif
2983 }
2984 
revealPath_App(const iString * path)2985 void revealPath_App(const iString *path) {
2986 #if defined (iPlatformAppleDesktop)
2987     const char *scriptPath = concatPath_CStr(dataDir_App_(), "revealfile.scpt");
2988     iFile *f = newCStr_File(scriptPath);
2989     if (open_File(f, writeOnly_FileMode | text_FileMode)) {
2990         /* AppleScript to select a specific file. */
2991         write_File(f, collect_Block(newCStr_Block("on run argv\n"
2992                                                   "  tell application \"Finder\"\n"
2993                                                   "    activate\n"
2994                                                   "    reveal POSIX file (item 1 of argv) as text\n"
2995                                                   "  end tell\n"
2996                                                   "end run\n")));
2997         close_File(f);
2998         iProcess *proc = new_Process();
2999         setArguments_Process(
3000             proc,
3001             iClob(newStringsCStr_StringList(
3002                 "/usr/bin/osascript", scriptPath, cstr_String(path), NULL)));
3003         start_Process(proc);
3004         iRelease(proc);
3005     }
3006     iRelease(f);
3007 #elif defined (iPlatformLinux) || defined (iPlatformHaiku)
3008     iFileInfo *inf = iClob(new_FileInfo(path));
3009     iRangecc target;
3010     if (isDirectory_FileInfo(inf)) {
3011         target = range_String(path);
3012     }
3013     else {
3014         target = dirName_Path(path);
3015     }
3016     iProcess *proc = new_Process();
3017     setArguments_Process(
3018         proc, iClob(newStringsCStr_StringList("/usr/bin/env", "xdg-open", cstr_Rangecc(target), NULL)));
3019     start_Process(proc);
3020     iRelease(proc);
3021 #else
3022     iAssert(0 /* File revealing not implemented on this platform */);
3023 #endif
3024 }
3025 
listDocuments_App(const iRoot * rootOrNull)3026 iObjectList *listDocuments_App(const iRoot *rootOrNull) {
3027     iWindow *win = get_Window();
3028     iObjectList *docs = new_ObjectList();
3029     iForIndices(i, win->roots) {
3030         iRoot *root = win->roots[i];
3031         if (!root) continue;
3032         if (!rootOrNull || root == rootOrNull) {
3033             const iWidget *tabs = findChild_Widget(root->widget, "doctabs");
3034             iForEach(ObjectList, i, children_Widget(findChild_Widget(tabs, "tabs.pages"))) {
3035                 if (isInstance_Object(i.object, &Class_DocumentWidget)) {
3036                     pushBack_ObjectList(docs, i.object);
3037                 }
3038             }
3039         }
3040     }
3041     return docs;
3042 }
3043 
listOpenURLs_App(void)3044 iStringSet *listOpenURLs_App(void) {
3045     iStringSet *set = new_StringSet();
3046     iObjectList *docs = listDocuments_App(NULL);
3047     iConstForEach(ObjectList, i, docs) {
3048         insert_StringSet(set, canonicalUrl_String(url_DocumentWidget(i.object)));
3049     }
3050     iRelease(docs);
3051     return set;
3052 }
3053 
mainWindow_App(void)3054 iMainWindow *mainWindow_App(void) {
3055     return app_.window;
3056 }
3057 
closePopups_App(void)3058 void closePopups_App(void) {
3059     iApp *d = &app_;
3060     const uint32_t now = SDL_GetTicks();
3061     iConstForEach(PtrArray, i, &d->popupWindows) {
3062         const iWindow *win = i.ptr;
3063         if (now - win->focusGainedAt > 200) {
3064             postCommand_Root(((const iWindow *) i.ptr)->roots[0], "cancel");
3065         }
3066     }
3067 }
3068