Lagrange [work/v1.10]

Inline download context menu; macOS: Show in Finder

=> e8f06bd0985ce2c9ac5ef02525672a426d559d18

diff --git a/po/en.po b/po/en.po
index 61d7d3f7..49f78eb8 100644
--- a/po/en.po
+++ b/po/en.po
@@ -854,6 +854,10 @@ msgstr "Bookmark Link…"
 msgid "link.download"
 msgstr "Download Linked File"
 
+# Shows where a local file is using the Finder.
+msgid "menu.reveal.macos"
+msgstr "Show in Finder"
+
 msgid "link.file.delete"
 msgstr "Delete File"
 
diff --git a/res/lang/cs.bin b/res/lang/cs.bin
index ec75dc3a..1c30a0a9 100644
Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ
diff --git a/res/lang/de.bin b/res/lang/de.bin
index 96fd003f..bf05a72e 100644
Binary files a/res/lang/de.bin and b/res/lang/de.bin differ
diff --git a/res/lang/en.bin b/res/lang/en.bin
index 26e3c36a..bd858ade 100644
Binary files a/res/lang/en.bin and b/res/lang/en.bin differ
diff --git a/res/lang/eo.bin b/res/lang/eo.bin
index 2df0e6b8..cc829562 100644
Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ
diff --git a/res/lang/es.bin b/res/lang/es.bin
index be919a78..f62291f1 100644
Binary files a/res/lang/es.bin and b/res/lang/es.bin differ
diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin
index d88b3386..f6f88d6c 100644
Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ
diff --git a/res/lang/fi.bin b/res/lang/fi.bin
index 5dba618c..e69245be 100644
Binary files a/res/lang/fi.bin and b/res/lang/fi.bin differ
diff --git a/res/lang/fr.bin b/res/lang/fr.bin
index 830bbc72..7be665e5 100644
Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ
diff --git a/res/lang/gl.bin b/res/lang/gl.bin
index 4df0c57c..8c1fdf24 100644
Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ
diff --git a/res/lang/hu.bin b/res/lang/hu.bin
index f4c826c8..b06c8676 100644
Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ
diff --git a/res/lang/ia.bin b/res/lang/ia.bin
index 6a18bd7d..4c3b403d 100644
Binary files a/res/lang/ia.bin and b/res/lang/ia.bin differ
diff --git a/res/lang/ie.bin b/res/lang/ie.bin
index 84cc2cd7..5e431c29 100644
Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ
diff --git a/res/lang/isv.bin b/res/lang/isv.bin
index 1a31a1f7..54cc6774 100644
Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ
diff --git a/res/lang/pl.bin b/res/lang/pl.bin
index a003ce03..cf6a6b23 100644
Binary files a/res/lang/pl.bin and b/res/lang/pl.bin differ
diff --git a/res/lang/ru.bin b/res/lang/ru.bin
index 807e032d..87c82a7d 100644
Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ
diff --git a/res/lang/sk.bin b/res/lang/sk.bin
index 33c83c09..3ac6e18c 100644
Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ
diff --git a/res/lang/sr.bin b/res/lang/sr.bin
index 7c4c8de1..5c66460d 100644
Binary files a/res/lang/sr.bin and b/res/lang/sr.bin differ
diff --git a/res/lang/tok.bin b/res/lang/tok.bin
index 601b07f3..19b6d9df 100644
Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ
diff --git a/res/lang/tr.bin b/res/lang/tr.bin
index 8cbaa556..9a8babc9 100644
Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ
diff --git a/res/lang/uk.bin b/res/lang/uk.bin
index 48adc59b..ebcb11be 100644
Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ
diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin
index 759e618a..ab4b338a 100644
Binary files a/res/lang/zh_Hans.bin and b/res/lang/zh_Hans.bin differ
diff --git a/res/lang/zh_Hant.bin b/res/lang/zh_Hant.bin
index 2bfc817b..f166156f 100644
Binary files a/res/lang/zh_Hant.bin and b/res/lang/zh_Hant.bin differ
diff --git a/src/app.c b/src/app.c
index b0b36783..e5f9a41c 100644
--- a/src/app.c
+++ b/src/app.c
@@ -55,6 +55,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -119,6 +120,7 @@ static const int idleThreshold_App_ = 1000; /* ms */
 struct Impl_App {
     iCommandLine args;
     iString *    execPath;
+    iStringSet * tempFilesPendingDeletion;
     iMimeHooks * mimehooks;
     iGmCerts *   certs;
     iVisited *   visited;
@@ -735,6 +737,7 @@ static void init_App_(iApp *d, int argc, char **argv) {
 #endif
     d->isDarkSystemTheme = iTrue; /* will be updated by system later on, if supported */
     d->isSuspended = iFalse;
+    d->tempFilesPendingDeletion = new_StringSet();
     init_CommandLine(&d->args, argc, argv);
     /* Where was the app started from? We ask SDL first because the command line alone 
        cannot be relied on (behavior differs depending on OS). */ {
@@ -1005,8 +1008,13 @@ static void deinit_App(iApp *d) {
 #endif
     deinit_SortedArray(&d->tickers);
     deinit_Periodic(&d->periodic);
-    deinit_Lang();
+    deinit_Lang();    
     iRecycle();
+    /* Delete all temporary files created while running. */
+    iConstForEach(StringSet, tmp, d->tempFilesPendingDeletion) {
+        remove(cstr_String(tmp.value));
+    }
+    iRelease(d->tempFilesPendingDeletion);
 }
 
 const iString *execPath_App(void) {
@@ -1082,6 +1090,17 @@ const iString *downloadPathForUrl_App(const iString *url, const iString *mime) {
     return collect_String(savePath);
 }
 
+const iString *temporaryPathForUrl_App(const iString *url, const iString *mime) {
+    iApp *d = &app_;
+    iString       *tmpPath = collectNewCStr_String(tmpnam(NULL));
+    const iRangecc tmpDir  = dirName_Path(tmpPath);
+    set_String(
+        tmpPath,
+        collect_String(concat_Path(collectNewRange_String(tmpDir), fileNameForUrl_App(url, mime))));
+    insert_StringSet(d->tempFilesPendingDeletion, tmpPath); /* deleted in `deinit_App` */
+    return tmpPath;
+}
+
 const iString *debugInfo_App(void) {
     extern char **environ; /* The environment variables. */
     iApp *d = &app_;
@@ -2687,7 +2706,9 @@ iBool handleCommand_App(const char *cmd) {
     }
 #endif
     else if (equal_Command(cmd, "downloads.open")) {
-        postCommandf_App("open url:%s", cstrCollect_String(makeFileUrl_String(downloadDir_App())));
+        postCommandf_App("open newtab:%d url:%s",
+                         argLabel_Command(cmd, "newtab"),
+                         cstrCollect_String(makeFileUrl_String(downloadDir_App())));
         return iTrue;
     }
     else if (equal_Command(cmd, "ca.file")) {
@@ -2718,6 +2739,19 @@ iBool handleCommand_App(const char *cmd) {
         }
         return iTrue;
     }
+    else if (equal_Command(cmd, "reveal")) {
+        const iString *path = NULL;
+        if (hasLabel_Command(cmd, "path")) {
+            path = suffix_Command(cmd, "path");
+        }
+        else if (hasLabel_Command(cmd, "url")) {
+            path = collect_String(localFilePathFromUrl_String(suffix_Command(cmd, "url")));
+        }
+        if (path) {
+            revealPath_App(path);
+        }
+        return iTrue;
+    }
     else if (equal_Command(cmd, "open")) {
         const char *urlArg = suffixPtr_Command(cmd, "url");
         if (!urlArg) {
@@ -2726,9 +2760,8 @@ iBool handleCommand_App(const char *cmd) {
         if (findWidget_App("prefs")) {
             postCommand_App("prefs.dismiss");        
         }
-        iString    *url         = collectNewCStr_String(urlArg);
-        const iBool noProxy     = argLabel_Command(cmd, "noproxy") != 0;
-        const iBool fromSidebar = argLabel_Command(cmd, "fromsidebar") != 0;
+        iString    *url     = collectNewCStr_String(urlArg);
+        const iBool noProxy = argLabel_Command(cmd, "noproxy") != 0;
         iUrl parts;
         init_Url(&parts, url);
         if (equal_Rangecc(parts.scheme, "about") && equal_Rangecc(parts.path, "command") &&
@@ -3354,26 +3387,11 @@ void openInDefaultBrowser_App(const iString *url) {
 
 void revealPath_App(const iString *path) {
 #if defined (iPlatformAppleDesktop)
-    const char *scriptPath = concatPath_CStr(dataDir_App_(), "revealfile.scpt");
-    iFile *f = newCStr_File(scriptPath);
-    if (open_File(f, writeOnly_FileMode | text_FileMode)) {
-        /* AppleScript to select a specific file. */
-        write_File(f, collect_Block(newCStr_Block("on run argv\n"
-                                                  "  tell application \"Finder\"\n"
-                                                  "    activate\n"
-                                                  "    reveal POSIX file (item 1 of argv) as text\n"
-                                                  "  end tell\n"
-                                                  "end run\n")));
-        close_File(f);
-        iProcess *proc = new_Process();
-        setArguments_Process(
-            proc,
-            iClob(newStringsCStr_StringList(
-                "/usr/bin/osascript", scriptPath, cstr_String(path), NULL)));
-        start_Process(proc);
-        iRelease(proc);
-    }
-    iRelease(f);
+    iProcess *proc = new_Process();
+    setArguments_Process(
+        proc, iClob(newStringsCStr_StringList("/usr/bin/open", "-R", cstr_String(path), NULL)));
+    start_Process(proc);
+    iRelease(proc);
 #elif defined (iPlatformLinux) || defined (iPlatformHaiku)
     iFileInfo *inf = iClob(new_FileInfo(path));
     iRangecc target;
diff --git a/src/app.h b/src/app.h
index 0c336e65..d15e1f21 100644
--- a/src/app.h
+++ b/src/app.h
@@ -111,6 +111,7 @@ const iString *     schemeProxy_App     (iRangecc scheme);
 iBool               willUseProxy_App    (const iRangecc scheme);
 const iString *     searchQueryUrl_App  (const iString *queryStringUnescaped);
 const iString *     fileNameForUrl_App  (const iString *url, const iString *mime);
+const iString *     temporaryPathForUrl_App(const iString *url, const iString *mime); /* deleted before quitting */
 const iString *     downloadPathForUrl_App(const iString *url, const iString *mime);
 
 typedef void (*iTickerFunc)(iAny *);
diff --git a/src/gmcerts.c b/src/gmcerts.c
index 8f7bf181..7b05103b 100644
--- a/src/gmcerts.c
+++ b/src/gmcerts.c
@@ -654,7 +654,7 @@ void importIdentity_GmCerts(iGmCerts *d, iTlsCertificate *cert, const iString *n
 }
 
 static const char *certPath_GmCerts_(const iGmCerts *d, const iGmIdentity *identity) {
-    if (!(identity->flags & (temporary_GmIdentityFlag | imported_GmIdentityFlag))) {
+    if (!(identity->flags & temporary_GmIdentityFlag)) {
         const char *finger = cstrCollect_String(hexEncode_Block(&identity->fingerprint));
         return concatPath_CStr(cstr_String(&d->saveDir), format_CStr("idents/%s", finger));
     }
diff --git a/src/ui/certlistwidget.c b/src/ui/certlistwidget.c
index 31d8bac6..5a1c481b 100644
--- a/src/ui/certlistwidget.c
+++ b/src/ui/certlistwidget.c
@@ -97,17 +97,21 @@ static void updateContextMenu_CertListWidget_(iCertListWidget *d) {
         pushBack_Array(items, &(iMenuItem){ format_CStr("```%s", cstr_String(docUrl)) });
         firstIndex = 1;
     }
-    pushBackN_Array(items, (iMenuItem[]){
+    const iMenuItem ctxItems[] = {
         { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" },
         { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" },
         { close_Icon " ${ident.stopuse.all}", 0, 0, "ident.use arg:0 clear:1" },
         { "---", 0, 0, NULL },
         { edit_Icon " ${menu.edit.notes}", 0, 0, "ident.edit" },
         { "${ident.fingerprint}", 0, 0, "ident.fingerprint" },
+#if defined (iPlatformAppleDesktop)
+        { magnifyingGlass_Icon " ${menu.reveal.macos}", 0, 0, "ident.reveal" },
+#endif
         { export_Icon " ${ident.export}", 0, 0, "ident.export" },
         { "---", 0, 0, NULL },
         { delete_Icon " " uiTextCaution_ColorEscape "${ident.delete}", 0, 0, "ident.delete confirm:1" },
-    }, 9);
+    };
+    pushBackN_Array(items, ctxItems, iElemCount(ctxItems));
     /* Used URLs. */
     const iGmIdentity *ident = menuIdentity_CertListWidget_(d);
     if (ident) {
@@ -244,7 +248,7 @@ static iBool processEvent_CertListWidget_(iCertListWidget *d, const SDL_Event *e
             if (ident) {
                 const iString *crtPath = certificatePath_GmCerts(certs_App(), ident);
                 if (crtPath) {
-                    revealPath_App(crtPath);
+                    postCommandf_App("reveal path:%s", cstr_String(crtPath));
                 }
             }
             return iTrue;
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 3771dd6c..9e5e6ea3 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -4125,16 +4125,16 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
         }
         else if (!isEmpty_Block(&d->sourceContent)) {
             if (argLabel_Command(cmd, "extview")) {
-                iString       *tmpPath = collectNewCStr_String(tmpnam(NULL));
-                const iRangecc tmpDir  = dirName_Path(tmpPath);
-                set_String(
-                    tmpPath,
-                    collect_String(concat_Path(collectNewRange_String(tmpDir),
-                                               fileNameForUrl_App(d->mod.url, &d->sourceMime))));
-                if (saveToFile_(tmpPath, &d->sourceContent, iFalse)) {
-                    /* TODO: Remember this temporary path and delete it when quitting the app. */
-                    postCommandf_Root(w->root, "!open default:1 url:%s",
-                                      cstrCollect_String(makeFileUrl_String(tmpPath)));
+                if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) {
+                    /* Already a file so just open it directly. */
+                    postCommandf_Root(w->root, "!open default:1 url:%s", cstr_String(d->mod.url));
+                }
+                else {
+                    const iString *tmpPath = temporaryPathForUrl_App(d->mod.url, &d->sourceMime);
+                    if (saveToFile_(tmpPath, &d->sourceContent, iFalse)) {
+                        postCommandf_Root(w->root, "!open default:1 url:%s",
+                                          cstrCollect_String(makeFileUrl_String(tmpPath)));
+                    }
                 }
             }
             else {
@@ -4484,11 +4484,6 @@ static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Ev
         ev->type != SDL_MOUSEMOTION) {
         return iFalse;
     }
-    if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) {
-        if (ev->button.button != SDL_BUTTON_LEFT) {
-            return iFalse;
-        }
-    }
     if (d->grabbedPlayer) {
         /* Updated in the drag. */
         return iFalse;
@@ -4496,9 +4491,23 @@ static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Ev
     const iInt2 mouse = init_I2(ev->button.x, ev->button.y);
     iConstForEach(PtrArray, i, &d->view.visibleMedia) {
         const iGmRun *run  = i.ptr;
+        if (run->mediaType == download_MediaType) {
+            iDownloadUI ui;
+            init_DownloadUI(&ui, media_GmDocument(d->view.doc), mediaId_GmRun(run).id,
+                            runRect_DocumentView_(&d->view, run));
+            if (processEvent_DownloadUI(&ui, ev)) {
+                return iTrue;
+            }
+            continue;
+        }
         if (run->mediaType != audio_MediaType) {
             continue;
         }
+        if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) {
+            if (ev->button.button != SDL_BUTTON_LEFT) {
+                return iFalse;
+            }
+        }
         /* TODO: move this to mediaui.c */
         const iRect rect = runRect_DocumentView_(&d->view, run);
         iPlayer *   plr  = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
@@ -4842,6 +4851,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
         iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse);
         return iTrue;
     }
+    if (processMediaEvents_DocumentWidget_(d, ev)) {
+        return iTrue;
+    }
     if (ev->type == SDL_MOUSEBUTTONDOWN) {
         if (ev->button.button == SDL_BUTTON_X1) {
             postCommand_Root(w->root, "navigate.back");
@@ -4923,6 +4935,23 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
                         if (deviceType_App() == phone_AppDeviceType) {
                             removeN_Array(&items, size_Array(&items) - 2, iInvalidSize);
                         }
+                        if (equalCase_Rangecc(scheme, "file")) {
+                            pushBack_Array(&items, &(iMenuItem){ "---" });
+                            pushBack_Array(&items,
+                                           &(iMenuItem){ export_Icon " ${menu.open.external}",
+                                                         0,
+                                                         0,
+                                                         format_CStr("!open default:1 url:%s",
+                                                                     cstr_String(linkUrl)) });
+#if defined (iPlatformAppleDesktop)
+                            pushBack_Array(&items,
+                                           &(iMenuItem){ "${menu.reveal.macos}",
+                                                         0,
+                                                         0,
+                                                         format_CStr("!reveal url:%s",
+                                                                     cstr_String(linkUrl)) });
+#endif
+                        }                        
                     }
                     else if (!willUseProxy_App(scheme)) {
                         pushBack_Array(
@@ -4959,7 +4988,8 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
                                                                  cstr_String(linkUrl)) },
                                                    },
                                     3);
-                    if (isNative && d->contextLink->mediaType != download_MediaType) {
+                    if (isNative && d->contextLink->mediaType != download_MediaType &&
+                        !equalCase_Rangecc(scheme, "file")) {
                         pushBackN_Array(&items, (iMenuItem[]){
                             { "---" },
                             { download_Icon " ${link.download}", 0, 0, "document.downloadlink" },
@@ -4979,6 +5009,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
                     }
                     if (equalCase_Rangecc(scheme, "file")) {
                         /* Local files may be deleted. */
+                        pushBack_Array(&items, &(iMenuItem){ "---" });
                         pushBack_Array(
                             &items,
                             &(iMenuItem){ delete_Icon " " uiTextCaution_ColorEscape
@@ -5052,9 +5083,6 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
             processContextMenuEvent_Widget(d->menu, ev, {});
         }
     }
-    if (processMediaEvents_DocumentWidget_(d, ev)) {
-        return iTrue;
-    }
     if (processEvent_Banner(d->banner, ev)) {
         return iTrue;
     }
diff --git a/src/ui/linkinfo.c b/src/ui/linkinfo.c
index cb48c7ea..5102f9b3 100644
--- a/src/ui/linkinfo.c
+++ b/src/ui/linkinfo.c
@@ -92,7 +92,7 @@ void infoText_LinkInfo(const iGmDocument *doc, iGmLinkId linkId, iString *text_o
         appendRange_String(text_out, (iRangecc){ parts.path.start, constEnd_String(url) });
     }
     else if (scheme != gemini_GmLinkScheme) {
-        appendCStr_String(text_out, globe_Icon " ");
+        appendCStr_String(text_out, scheme == file_GmLinkScheme ? "" : globe_Icon " ");
         append_String(text_out, url);
     }
     else {
diff --git a/src/ui/mediaui.c b/src/ui/mediaui.c
index 1c828194..2aec568f 100644
--- a/src/ui/mediaui.c
+++ b/src/ui/mediaui.c
@@ -238,6 +238,44 @@ void init_DownloadUI(iDownloadUI *d, const iMedia *media, uint16_t mediaId, iRec
 /*----------------------------------------------------------------------------------------------*/
 
 iBool processEvent_DownloadUI(iDownloadUI *d, const SDL_Event *ev) {
+    if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) {
+        const iInt2 mouse = init_I2(ev->button.x, ev->button.y);
+        if (!contains_Rect(d->bounds, mouse)) {
+            return iFalse;
+        }
+        float bytesPerSecond;
+        const iString *path;
+        iBool isFinished;
+        downloadStats_Media(d->media, (iMediaId){ download_MediaType, d->mediaId },
+                            &path, &bytesPerSecond, &isFinished);
+        if (isFinished) {
+            if (ev->button.button == SDL_BUTTON_RIGHT && ev->type == SDL_MOUSEBUTTONDOWN) {
+                const iMenuItem items[] = {
+                    /* Items related to the file */
+                    { openTab_Icon " ${menu.opentab}",
+                      0,
+                      0,
+                      format_CStr("!open newtab:1 url:%s",
+                                  cstrCollect_String(makeFileUrl_String(path))) },
+#if defined (iPlatformAppleDesktop)
+                    { "${menu.reveal.macos}",
+                      0,
+                      0,
+                      format_CStr("!reveal path:%s", cstr_String(path)) },
+#endif
+                    { "---" },
+                    /* Generic items */
+                    { "${menu.downloads}", 0, 0, "downloads.open newtab:1" },
+                };
+                openMenu_Widget(makeMenu_Widget(get_Root()->widget, items, iElemCount(items)),
+                                mouse);
+                return iTrue;
+            }
+            else if (ev->button.button == SDL_BUTTON_LEFT && ev->type == SDL_MOUSEBUTTONUP) {
+                postCommandf_App("open default:1 url:%s", cstrCollect_String(makeFileUrl_String(path)));
+            }
+        }
+    }
     return iFalse;
 }
 
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.10/cdiff/e8f06bd0985ce2c9ac5ef02525672a426d559d18
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
96.564159 milliseconds
Gemini-to-HTML Time
1.114746 milliseconds

This content has been proxied by September (3851b).