Lagrange [work/v1.17]

Added preference for maximum cache size

=> eda45fcd34189e6844babde1ebc60c083b1b09da

diff --git a/src/app.c b/src/app.c
index 5d27618b..82628c9c 100644
--- a/src/app.c
+++ b/src/app.c
@@ -194,6 +194,7 @@ static iString *serializePrefs_App_(const iApp *d) {
     appendFormat_String(str, "zoom.set arg:%d\n", d->prefs.zoomPercent);
     appendFormat_String(str, "smoothscroll arg:%d\n", d->prefs.smoothScrolling);
     appendFormat_String(str, "imageloadscroll arg:%d\n", d->prefs.loadImageInsteadOfScrolling);
+    appendFormat_String(str, "cachesize.set arg:%d\n", d->prefs.maxCacheSize);
     appendFormat_String(str, "decodeurls arg:%d\n", d->prefs.decodeUserVisibleURLs);
     appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth);
     appendFormat_String(str, "prefs.biglede.changed arg:%d\n", d->prefs.bigFirstParagraph);
@@ -321,6 +322,7 @@ iObjectList *listDocuments_App(void) {
 
 static void saveState_App_(const iApp *d) {
     iUnused(d);
+    trimCache_App();
     iFile *f = newCStr_File(concatPath_CStr(dataDir_App_, stateFileName_App_));
     if (open_File(f, writeOnly_FileMode)) {
         writeData_File(f, magicState_App_, 4);
@@ -503,7 +505,7 @@ const iString *debugInfo_App(void) {
     iString *msg = collectNew_String();
     format_String(msg, "# Debug information\n");
     appendFormat_String(msg, "## Documents\n");
-    iForEach(ObjectList, k, listDocuments_App()) {
+    iForEach(ObjectList, k, iClob(listDocuments_App())) {
         iDocumentWidget *doc = k.object;
         appendFormat_String(msg, "### Tab %zu: %s\n",
                             childIndex_Widget(constAs_Widget(doc)->parent, k.object),
@@ -857,6 +859,8 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
                          isSelected_Widget(findChild_Widget(d, "prefs.ostheme")));
         postCommandf_App("decodeurls arg:%d",
                          isSelected_Widget(findChild_Widget(d, "prefs.decodeurls")));
+        postCommandf_App("cachesize.set arg:%d",
+                         toInt_String(text_InputWidget(findChild_Widget(d, "prefs.cachesize"))));
         postCommandf_App("proxy.gemini address:%s",
                          cstr_String(text_InputWidget(findChild_Widget(d, "prefs.proxy.gemini"))));
         postCommandf_App("proxy.gopher address:%s",
@@ -940,6 +944,33 @@ iDocumentWidget *newTab_App(const iDocumentWidget *duplicateOf, iBool switchToNe
     return doc;
 }
 
+void trimCache_App(void) {
+    iApp *d = &app_;
+    size_t cacheSize = 0;
+    const size_t limit = d->prefs.maxCacheSize * 1000000;
+    iObjectList *docs = listDocuments_App();
+    iForEach(ObjectList, i, docs) {
+        cacheSize += cacheSize_History(history_DocumentWidget(i.object));
+    }
+    init_ObjectListIterator(&i, docs);
+    iBool wasPruned = iFalse;
+    while (cacheSize > limit) {
+        iDocumentWidget *doc = i.object;
+        const size_t pruned = pruneLeastImportant_History(history_DocumentWidget(doc));
+        if (pruned) {
+            cacheSize -= pruned;
+            wasPruned = iTrue;
+        }
+        next_ObjectListIterator(&i);
+        if (!i.value) {
+            if (!wasPruned) break;
+            wasPruned = iFalse;
+            init_ObjectListIterator(&i, docs);
+        }
+    }
+    iRelease(docs);
+}
+
 static iBool handleIdentityCreationCommands_(iWidget *dlg, const char *cmd) {
     iApp *d = &app_;
     if (equal_Command(cmd, "ident.temp.changed")) {
@@ -1154,6 +1185,13 @@ iBool handleCommand_App(const char *cmd) {
         postCommandf_App("theme.changed auto:1");
         return iTrue;
     }
+    else if (equal_Command(cmd, "cachesize.set")) {
+        d->prefs.maxCacheSize = arg_Command(cmd);
+        if (d->prefs.maxCacheSize <= 0) {
+            d->prefs.maxCacheSize = 0;
+        }
+        return iTrue;
+    }
     else if (equal_Command(cmd, "proxy.gemini")) {
         setCStr_String(&d->prefs.geminiProxy, suffixPtr_Command(cmd, "address"));
         return iTrue;
@@ -1331,6 +1369,8 @@ iBool handleCommand_App(const char *cmd) {
                 dlg, format_CStr("prefs.saturation.%d", (int) (d->prefs.saturation * 3.99f))),
             selected_WidgetFlag,
             iTrue);
+        setText_InputWidget(findChild_Widget(dlg, "prefs.cachesize"),
+                            collectNewFormat_String("%d", d->prefs.maxCacheSize));
         setToggle_Widget(findChild_Widget(dlg, "prefs.decodeurls"), d->prefs.decodeUserVisibleURLs);
         setText_InputWidget(findChild_Widget(dlg, "prefs.proxy.gemini"), &d->prefs.geminiProxy);
         setText_InputWidget(findChild_Widget(dlg, "prefs.proxy.gopher"), &d->prefs.gopherProxy);
diff --git a/src/app.h b/src/app.h
index 0e8351aa..efaf0a3e 100644
--- a/src/app.h
+++ b/src/app.h
@@ -61,20 +61,20 @@ void        refresh_App                 (void);
 iBool       isRefreshPending_App        (void);
 uint32_t    elapsedSinceLastTicker_App  (void); /* milliseconds */
 
-const iPrefs *      prefs_App           (void);
-iBool               forceSoftwareRender_App(void);
-enum iColorTheme    colorTheme_App      (void);
-const iString *     schemeProxy_App     (iRangecc scheme);
-iBool               willUseProxy_App    (const iRangecc scheme);
-
-iMimeHooks *        mimeHooks_App       (void);
 iGmCerts *          certs_App           (void);
 iVisited *          visited_App         (void);
 iBookmarks *        bookmarks_App       (void);
+iMimeHooks *        mimeHooks_App       (void);
 iDocumentWidget *   document_App        (void);
 iObjectList *       listDocuments_App   (void);
-iDocumentWidget *   document_Command    (const char *cmd);
 iDocumentWidget *   newTab_App          (const iDocumentWidget *duplicateOf, iBool switchToNew);
+void                trimCache_App       (void);
+
+const iPrefs *      prefs_App           (void);
+iBool               forceSoftwareRender_App(void);
+enum iColorTheme    colorTheme_App      (void);
+const iString *     schemeProxy_App     (iRangecc scheme);
+iBool               willUseProxy_App    (const iRangecc scheme);
 
 typedef void (*iTickerFunc)(iAny *);
 
@@ -91,5 +91,7 @@ iLocalDef void postCommandString_App(const iString *command) {
     }
 }
 
+iDocumentWidget *   document_Command    (const char *cmd);
+
 void        openInDefaultBrowser_App    (const iString *url);
 void        revealPath_App              (const iString *path);
diff --git a/src/history.c b/src/history.c
index 202549a9..59d515dc 100644
--- a/src/history.c
+++ b/src/history.c
@@ -27,6 +27,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #include 
 #include 
 #include 
+#include 
 
 static const size_t maxStack_History_ = 50; /* back/forward navigable items */
 
@@ -285,6 +286,48 @@ void setCachedResponse_History(iHistory *d, const iGmResponse *response) {
     unlock_Mutex(d->mtx);
 }
 
+size_t cacheSize_History(const iHistory *d) {
+    size_t cached = 0;
+    lock_Mutex(d->mtx);
+    iConstForEach(Array, i, &d->recent) {
+        const iRecentUrl *url = i.value;
+        if (url->cachedResponse) {
+            cached += size_Block(&url->cachedResponse->body);
+        }
+    }
+    unlock_Mutex(d->mtx);
+    return cached;
+}
+
+size_t pruneLeastImportant_History(iHistory *d) {
+    size_t delta  = 0;
+    size_t chosen = iInvalidPos;
+    double score  = 0.0f;
+    iTime now;
+    initCurrent_Time(&now);
+    lock_Mutex(d->mtx);
+    iConstForEach(Array, i, &d->recent) {
+        const iRecentUrl *url = i.value;
+        if (url->cachedResponse) {
+            const double urlScore =
+                size_Block(&url->cachedResponse->body) *
+                pow(secondsSince_Time(&now, &url->cachedResponse->when) / 60.0, 1.25);
+            if (urlScore > score) {
+                chosen = index_ArrayConstIterator(&i);
+                score  = urlScore;
+            }
+        }
+    }
+    if (chosen != iInvalidPos) {
+        iRecentUrl *url = at_Array(&d->recent, chosen);
+        delta = size_Block(&url->cachedResponse->body);
+        delete_GmResponse(url->cachedResponse);
+        url->cachedResponse = NULL;
+    }
+    unlock_Mutex(d->mtx);
+    return delta;
+}
+
 const iStringArray *searchContents_History(const iHistory *d, const iRegExp *pattern) {
     iStringArray *urls = iClob(new_StringArray());
     lock_Mutex(d->mtx);
diff --git a/src/history.h b/src/history.h
index 4c6507e3..7c2684f1 100644
--- a/src/history.h
+++ b/src/history.h
@@ -56,6 +56,7 @@ iBool       goForward_History           (iHistory *);
 iRecentUrl *recentUrl_History           (iHistory *, size_t pos);
 iRecentUrl *mostRecentUrl_History       (iHistory *);
 iRecentUrl *findUrl_History             (iHistory *, const iString *url);
+size_t      pruneLeastImportant_History (iHistory *);
 
 const iStringArray *   searchContents_History   (const iHistory *, const iRegExp *pattern); /* chronologically ascending */
 
@@ -67,6 +68,7 @@ const iRecentUrl *
             constMostRecentUrl_History  (const iHistory *);
 const iGmResponse *
             cachedResponse_History      (const iHistory *);
+size_t      cacheSize_History           (const iHistory *);
 
 iString *   debugInfo_History           (const iHistory *);
 
diff --git a/src/prefs.c b/src/prefs.c
index 188938a2..ce32962b 100644
--- a/src/prefs.c
+++ b/src/prefs.c
@@ -34,6 +34,7 @@ void init_Prefs(iPrefs *d) {
     d->smoothScrolling   = iTrue;
     d->loadImageInsteadOfScrolling = iFalse;
     d->decodeUserVisibleURLs = iTrue;
+    d->maxCacheSize      = 10;
     d->font              = nunito_TextFont;
     d->headingFont       = nunito_TextFont;
     d->monospaceGemini   = iFalse;
diff --git a/src/prefs.h b/src/prefs.h
index 07298eac..1c3274d9 100644
--- a/src/prefs.h
+++ b/src/prefs.h
@@ -49,6 +49,7 @@ struct Impl_Prefs {
     iBool            loadImageInsteadOfScrolling;
     /* Network */
     iBool            decodeUserVisibleURLs;
+    int              maxCacheSize; /* MB */
     iString          geminiProxy;
     iString          gopherProxy;
     iString          httpProxy;
diff --git a/src/ui/util.c b/src/ui/util.c
index 7fc27130..91945db8 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -1090,7 +1090,7 @@ iWidget *makePreferences_Widget(void) {
     }
     /* Colors. */ {
         appendTwoColumnPage_(tabs, "Colors", '3', &headings, &values);
-        makeTwoColumnHeading_("PAGE CONTENTS", headings, values);
+        makeTwoColumnHeading_("PAGE CONTENT", headings, values);
         for (int i = 0; i < 2; ++i) {
             const iBool isDark = (i == 0);
             const char *mode = isDark ? "dark" : "light";
@@ -1121,6 +1121,7 @@ iWidget *makePreferences_Widget(void) {
     }
     /* Layout. */ {
         appendTwoColumnPage_(tabs, "Style", '4', &headings, &values);
+        makeTwoColumnHeading_("FONTS", headings, values);
         /* Fonts. */ {
             iWidget *fonts;
             addChild_Widget(headings, iClob(makeHeading_Widget("Heading font:")));
@@ -1140,8 +1141,7 @@ iWidget *makePreferences_Widget(void) {
                 addChild_Widget(mono, iClob(makeToggle_Widget("prefs.mono.gopher"))), "Gopher");
             addChildFlags_Widget(values, iClob(mono), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
         }
-        addChild_Widget(headings, iClob(makePadding_Widget(2 * gap_UI)));
-        addChild_Widget(values, iClob(makePadding_Widget(2 * gap_UI)));
+        makeTwoColumnHeading_("PARAGRAPH", headings, values);
         addChild_Widget(headings, iClob(makeHeading_Widget("Line width:")));
         iWidget *widths = new_Widget();
         /* Line widths. */ {
@@ -1162,10 +1162,19 @@ iWidget *makePreferences_Widget(void) {
         addChild_Widget(headings, iClob(makeHeading_Widget("Big 1st paragaph:")));
         addChild_Widget(values, iClob(makeToggle_Widget("prefs.biglede")));
     }
-    /* Proxies. */ {
+    /* Network. */ {
         appendTwoColumnPage_(tabs, "Network", '5', &headings, &values);
-        addChild_Widget(headings, iClob(makeHeading_Widget("Decode paths:")));
+        addChild_Widget(headings, iClob(makeHeading_Widget("Cache size:")));
+        iWidget *cacheGroup = new_Widget(); {
+            iInputWidget *cache = new_InputWidget(4);
+            setSelectAllOnFocus_InputWidget(cache, iTrue);
+            setId_Widget(addChild_Widget(cacheGroup, iClob(cache)), "prefs.cachesize");
+            addChildFlags_Widget(cacheGroup, iClob(new_LabelWidget("MB", NULL)), frameless_WidgetFlag);
+        }
+        addChildFlags_Widget(values, iClob(cacheGroup), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
+        addChild_Widget(headings, iClob(makeHeading_Widget("Decode URLs:")));
         addChild_Widget(values, iClob(makeToggle_Widget("prefs.decodeurls")));
+        makeTwoColumnHeading_("PROXIES", headings, values);
         addChild_Widget(headings, iClob(makeHeading_Widget("Gemini proxy:")));
         setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.proxy.gemini");
         addChild_Widget(headings, iClob(makeHeading_Widget("Gopher proxy:")));
diff --git a/src/ui/window.c b/src/ui/window.c
index 2e38512b..66994e79 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -397,6 +397,7 @@ static iBool handleNavBarCommands_(iWidget *navBar, const char *cmd) {
             if (equal_Command(cmd, "document.changed")) {
                 iInputWidget *url = findWidget_App("url");
                 const iString *urlStr = collect_String(suffix_Command(cmd, "url"));
+                trimCache_App();
                 visitUrl_Visited(visited_App(), urlStr, 0);
                 postCommand_App("visited.changed"); /* sidebar will update */
                 setText_InputWidget(url, urlStr);
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.17/cdiff/eda45fcd34189e6844babde1ebc60c083b1b09da
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
26.359942 milliseconds
Gemini-to-HTML Time
0.645427 milliseconds

This content has been proxied by September (ba2dc).