Lagrange [release]

Added option to show URL paths as encoded or decoded

8864cc5aee3b134b0ffc5f53c1593645f4109f8c
diff --git a/res/about/version.gmi b/res/about/version.gmi
index 50ade298..42475e13 100644
--- a/res/about/version.gmi
+++ b/res/about/version.gmi
@@ -8,6 +8,9 @@
 
 ## 0.13
 * Support for Internationalized Domain Names (IDN) in network requests.
+* IDNs show up in decoded form in the UI.
+* Percent-encoded Unicode characters in URL paths are decoded for the UI, and encoded in outgoing requests.
+* Added option to disable decoding of percent-encoded paths.
 * Quick search via URL bar shows entries from subscribed feeds.
 * Added keybindings for zooming.
 * Improved usability of page content searching (${CTRL+}F, Escape).
diff --git a/src/app.c b/src/app.c
index 8a6b2a66..ae21d078 100644
--- a/src/app.c
+++ b/src/app.c
@@ -188,6 +188,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, "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);
     appendFormat_String(str, "prefs.sideicon.changed arg:%d\n", d->prefs.sideIcon);
@@ -836,6 +837,8 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
                          isSelected_Widget(findChild_Widget(d, "prefs.imageloadscroll")));
         postCommandf_App("ostheme arg:%d",
                          isSelected_Widget(findChild_Widget(d, "prefs.ostheme")));
+        postCommandf_App("decodeurls arg:%d",
+                         isSelected_Widget(findChild_Widget(d, "prefs.decodeurls")));
         postCommandf_App("proxy.gemini address:%s",
                          cstr_String(text_InputWidget(findChild_Widget(d, "prefs.proxy.gemini"))));
         postCommandf_App("proxy.gopher address:%s",
@@ -1094,6 +1097,10 @@ iBool handleCommand_App(const char *cmd) {
         d->prefs.smoothScrolling = arg_Command(cmd);
         return iTrue;
     }
+    else if (equal_Command(cmd, "decodeurls")) {
+        d->prefs.decodeUserVisibleURLs = arg_Command(cmd);
+        return iTrue;
+    }
     else if (equal_Command(cmd, "imageloadscroll")) {
         d->prefs.loadImageInsteadOfScrolling = arg_Command(cmd);
         return iTrue;
@@ -1184,7 +1191,7 @@ iBool handleCommand_App(const char *cmd) {
         return iTrue;
     }
     else if (equal_Command(cmd, "open")) {
-        const iString *url = collectNewCStr_String(suffixPtr_Command(cmd, "url"));
+        iString *url = collectNewCStr_String(suffixPtr_Command(cmd, "url"));
         const iBool noProxy = argLabel_Command(cmd, "noproxy");
         iUrl parts;
         init_Url(&parts, url);
@@ -1214,6 +1221,12 @@ iBool handleCommand_App(const char *cmd) {
         setInitialScroll_DocumentWidget(doc, argfLabel_Command(cmd, "scroll"));
         setRedirectCount_DocumentWidget(doc, redirectCount);
         setFlags_Widget(findWidget_App("document.progress"), hidden_WidgetFlag, iTrue);
+        if (prefs_App()->decodeUserVisibleURLs) {
+            urlDecodePath_String(url);
+        }
+        else {
+            urlEncodePath_String(url);
+        }
         setUrlFromCache_DocumentWidget(doc, url, isHistory);
         /* Optionally, jump to a text in the document. This will only work if the document
            is already available, e.g., it's from "about:" or restored from cache. */
@@ -1332,6 +1345,7 @@ iBool handleCommand_App(const char *cmd) {
                 dlg, format_CStr("prefs.saturation.%d", (int) (d->prefs.saturation * 3.99f))),
             selected_WidgetFlag,
             iTrue);
+        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);
         setText_InputWidget(findChild_Widget(dlg, "prefs.proxy.http"), &d->prefs.httpProxy);
diff --git a/src/gmrequest.c b/src/gmrequest.c
index 991485bc..1f922142 100644
--- a/src/gmrequest.c
+++ b/src/gmrequest.c
@@ -482,8 +482,6 @@ void deinit_GmRequest(iGmRequest *d) {
     deinit_Gopher(&d->gopher);
     delete_Audience(d->finished);
     delete_Audience(d->updated);
-//    delete_GmResponse(d->respPub);
-//    deinit_GmResponse(&d->respInt);
     delete_GmResponse(d->resp);
     deinit_String(&d->url);
     delete_Mutex(d->mtx);
@@ -494,6 +492,10 @@ void setUrl_GmRequest(iGmRequest *d, const iString *url) {
     /* Encode hostname to Punycode here because we want to submit the Punycode domain name
        in the request. (TODO: Pending possible Gemini spec change.) */
     punyEncodeUrlHost_String(&d->url);
+    /* TODO: Gemini spec allows UTF-8 encoded URLs, but still need to percent-encode non-ASCII
+       characters? Could be a server-side issue, e.g., if they're using a URL parser meant for
+       the web. */
+    urlEncodePath_String(&d->url);
     urlEncodeSpaces_String(&d->url);
 }
 
diff --git a/src/gmutil.c b/src/gmutil.c
index 68525be9..afca4978 100644
--- a/src/gmutil.c
+++ b/src/gmutil.c
@@ -151,6 +151,42 @@ static iString *punyDecodeHost_(iRangecc host) {
     return result;
 }
 
+void urlDecodePath_String(iString *d) {
+    iUrl url;
+    init_Url(&url, d);
+    if (isEmpty_Range(&url.path)) {
+        return;
+    }
+    iString *decoded = new_String();
+    appendRange_String(decoded, (iRangecc){ constBegin_String(d), url.path.start });
+    iString *path    = newRange_String(url.path);
+    iString *decPath = urlDecode_String(path);
+    append_String(decoded, decPath);
+    delete_String(decPath);
+    delete_String(path);
+    appendRange_String(decoded, (iRangecc){ url.path.end, constEnd_String(d) });
+    set_String(d, decoded);
+    delete_String(decoded);
+}
+
+void urlEncodePath_String(iString *d) {
+    iUrl url;
+    init_Url(&url, d);
+    if (isEmpty_Range(&url.path)) {
+        return;
+    }
+    iString *encoded = new_String();
+    appendRange_String(encoded , (iRangecc){ constBegin_String(d), url.path.start });
+    iString *path    = newRange_String(url.path);
+    iString *encPath = urlEncodeExclude_String(path, "%/ ");
+    append_String(encoded, encPath);
+    delete_String(encPath);
+    delete_String(path);
+    appendRange_String(encoded, (iRangecc){ url.path.end, constEnd_String(d) });
+    set_String(d, encoded);
+    delete_String(encoded);
+}
+
 const iString *absoluteUrl_String(const iString *d, const iString *urlMaybeRelative) {
     iUrl orig;
     iUrl rel;
diff --git a/src/gmutil.h b/src/gmutil.h
index bbadbafd..88ed1f32 100644
--- a/src/gmutil.h
+++ b/src/gmutil.h
@@ -104,6 +104,8 @@ iRangecc        urlScheme_String        (const iString *);
 iRangecc        urlHost_String          (const iString *);
 const iString * absoluteUrl_String      (const iString *, const iString *urlMaybeRelative);
 void            punyEncodeUrlHost_String(iString *);
+void            urlDecodePath_String    (iString *);
+void            urlEncodePath_String    (iString *);
 iString *       makeFileUrl_String      (const iString *localFilePath);
 const char *    makeFileUrl_CStr        (const char *localFilePath);
 void            urlEncodeSpaces_String  (iString *);
diff --git a/src/prefs.c b/src/prefs.c
index 574e07d0..31ffe03b 100644
--- a/src/prefs.c
+++ b/src/prefs.c
@@ -33,6 +33,7 @@ void init_Prefs(iPrefs *d) {
     d->hoverOutline      = iFalse;
     d->smoothScrolling   = iTrue;
     d->loadImageInsteadOfScrolling = iFalse;
+    d->decodeUserVisibleURLs = iTrue;
     d->font              = nunito_TextFont;
     d->headingFont       = nunito_TextFont;
     d->monospaceGemini   = iFalse;
diff --git a/src/prefs.h b/src/prefs.h
index 3f4f534f..e95a32da 100644
--- a/src/prefs.h
+++ b/src/prefs.h
@@ -48,6 +48,7 @@ struct Impl_Prefs {
     iBool            smoothScrolling;
     iBool            loadImageInsteadOfScrolling;
     /* Network */
+    iBool            decodeUserVisibleURLs;
     iString          geminiProxy;
     iString          gopherProxy;
     iString          httpProxy;
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index 8024b240..1167ebe3 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -240,6 +240,13 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
                 const iVisitedUrl *visit = i.ptr;
                 iSidebarItem *item = new_SidebarItem();
                 set_String(&item->url, &visit->url);
+                set_String(&item->label, &visit->url);
+                if (prefs_App()->decodeUserVisibleURLs) {
+                    urlDecodePath_String(&item->label);
+                }
+                else {
+                    urlEncodePath_String(&item->label);
+                }
                 iDate date;
                 init_Date(&date, &visit->when);
                 if (date.day != on.day || date.month != on.month || date.year != on.year) {
@@ -1211,7 +1218,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
         }
         else {
             iUrl parts;
-            init_Url(&parts, &d->url);
+            init_Url(&parts, &d->label);
             const iBool isAbout  = equalCase_Rangecc(parts.scheme, "about");
             const iBool isGemini = equalCase_Rangecc(parts.scheme, "gemini");
             draw_Text(font,
diff --git a/src/ui/util.c b/src/ui/util.c
index 1ad3f30e..6c9d75dc 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -1136,7 +1136,9 @@ iWidget *makePreferences_Widget(void) {
         addChild_Widget(values, iClob(makeToggle_Widget("prefs.biglede")));
     }
     /* Proxies. */ {
-        appendTwoColumnPage_(tabs, "Proxies", '5', &headings, &values);
+        appendTwoColumnPage_(tabs, "Network", '5', &headings, &values);
+        addChild_Widget(headings, iClob(makeHeading_Widget("Decode paths:")));
+        addChild_Widget(values, iClob(makeToggle_Widget("prefs.decodeurls")));
         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:")));