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