Lagrange [dev]

TUI: Terminal UI improvements

=> 852cc782f198e250ba30f8b3974a39a66cbd1164

diff --git a/po/en.po b/po/en.po
index 476ed3f1..cc4a765d 100644
--- a/po/en.po
+++ b/po/en.po
@@ -2639,4 +2639,38 @@ msgstr "Content:"
 msgid "snip.accept"
 msgstr "Save Snippet"
 
+# Keyboard shortcut for the terminal. Should be very short or abbreviated.
+msgid "term.url"
+msgstr "Enter URL"
 
+# Keyboard shortcut for the terminal. Should be very short or abbreviated.
+msgid "term.linkkeys"
+msgstr "Open link"
+
+# Keyboard shortcut for the terminal. Should be very short or abbreviated.
+msgid "term.menu"
+msgstr "Context menu"
+
+# Keyboard shortcut for the terminal. Should be very short or abbreviated.
+msgid "term.menubar"
+msgstr "Menubar"
+
+# Keyboard shortcut for the terminal. Should be very short or abbreviated.
+msgid "term.sidebar"
+msgstr "Sidebar"
+
+# Keyboard shortcut for the terminal. Should be very short or abbreviated.
+msgid "term.tab.new"
+msgstr "New tab"
+
+# Keyboard shortcut for the terminal. Should be very short or abbreviated.
+msgid "term.tab.close"
+msgstr "Close tab"
+
+# Keyboard shortcut for the terminal. Should be very short or abbreviated.
+msgid "term.hover"
+msgstr "Focus link"
+
+# Keyboard shortcut for the terminal. Should be very short or abbreviated.
+msgid "term.focus"
+msgstr "Focus"
diff --git a/res/lang/cs.bin b/res/lang/cs.bin
index 11b3db07..69e51914 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 de4474ad..f30fc5a9 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 0f65af65..8bbb2026 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 ab5944c7..857dfe5d 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 845a1738..395f14fb 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 a89c6e88..7faaa660 100644
Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ
diff --git a/res/lang/eu.bin b/res/lang/eu.bin
index 6a4aebfc..8b27e451 100644
Binary files a/res/lang/eu.bin and b/res/lang/eu.bin differ
diff --git a/res/lang/fi.bin b/res/lang/fi.bin
index 05a48ac9..3f1b14f0 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 e770bade..26241003 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 65e28044..018f8641 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 35bb3d0d..c368d0fa 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 df40df5e..764728a6 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 56fffd51..b803de9f 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 44ab74aa..4a223ab5 100644
Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ
diff --git a/res/lang/it.bin b/res/lang/it.bin
index aafc26ba..ed8d0f77 100644
Binary files a/res/lang/it.bin and b/res/lang/it.bin differ
diff --git a/res/lang/ja.bin b/res/lang/ja.bin
index 324318b3..39bedf71 100644
Binary files a/res/lang/ja.bin and b/res/lang/ja.bin differ
diff --git a/res/lang/nl.bin b/res/lang/nl.bin
index ed6507ca..3b6718b3 100644
Binary files a/res/lang/nl.bin and b/res/lang/nl.bin differ
diff --git a/res/lang/pl.bin b/res/lang/pl.bin
index 0323b8f1..2ea179ba 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 a0775e00..a7725824 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 11041074..6414aec7 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 4bdc4370..8d206933 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 44647694..8b500280 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 c9a52449..34e69087 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 21504e85..b58b82dd 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 e278e641..be83359f 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 fbb33027..83010daa 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 a5e417d2..c4b07d44 100644
--- a/src/app.c
+++ b/src/app.c
@@ -2784,7 +2784,7 @@ iBool moveFocusInsideMenu_App(const void *sdlEvent) {
             }
             return iTrue;
         }
-        else {
+        else if (menu) {
             setCurrent_Window(window_Widget(focus_Widget()));
             postCommand_Widget(focus_Widget(), "cancel");
         }
diff --git a/src/defs.h b/src/defs.h
index bc5e0b95..3746acb0 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -332,6 +332,10 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {
 #   define LAGRANGE_MENUBAR
 #endif
 
+#if defined (iPlatformDesktop) && !defined (iPlatformTerminal)
+#   define LAGRANGE_MULTIPLE_WINDOWS
+#endif
+
 /* UI labels that depend on the platform */
 
 #if defined (iPlatformMobile)
diff --git a/src/ui/certlistwidget.c b/src/ui/certlistwidget.c
index 9da7fe6a..dbaefeb1 100644
--- a/src/ui/certlistwidget.c
+++ b/src/ui/certlistwidget.c
@@ -120,7 +120,7 @@ static void updateContextMenu_CertListWidget_(iCertListWidget *d) {
             insert_Array(items, insertPos++, &(iMenuItem){ "---", 0, 0, NULL });
         }
         iBool usedOnCurrentPage = iFalse;
-        iConstForEach(StringSet, i, ident->useUrls) {            
+        iConstForEach(StringSet, i, ident->useUrls) {
             const iString *url = i.value;
             usedOnCurrentPage |= startsWithCase_String(docUrl, cstr_String(url));
             iRangecc urlStr = range_String(url);
@@ -141,8 +141,8 @@ static void updateContextMenu_CertListWidget_(iCertListWidget *d) {
             remove_Array(items, firstIndex);
         }
     }
-    destroy_Widget(d->menu);    
-    d->menu = makeMenu_Widget(as_Widget(d), data_Array(items), size_Array(items));    
+    destroy_Widget(d->menu);
+    d->menu = makeMenu_Widget(as_Widget(d), data_Array(items), size_Array(items));
 }
 
 static void itemClicked_CertListWidget_(iCertListWidget *d, iCertItem *item, size_t itemIndex) {
@@ -180,7 +180,7 @@ static iBool processEvent_CertListWidget_(iCertListWidget *d, const SDL_Event *e
             return iTrue;
         }
         else if (isCommand_Widget(w, ev, "ident.use")) {
-            iGmIdentity *ident = menuIdentity_CertListWidget_(d);            
+            iGmIdentity *ident = menuIdentity_CertListWidget_(d);
             const iString *tabUrl = urlQueryStripped_String(url_DocumentWidget(document_App()));
             if (ident) {
                 if (argLabel_Command(cmd, "clear")) {
@@ -217,7 +217,16 @@ static iBool processEvent_CertListWidget_(iCertListWidget *d, const SDL_Event *e
             if (ident) {
                 const iString *fps = collect_String(
                     hexEncode_Block(collect_Block(fingerprint_TlsCertificate(ident->cert))));
-                SDL_SetClipboardText(cstr_String(fps));
+                if (isTerminal_Platform()) {
+                    makeMessage_Widget(
+                        "${ident.fingerprint}",
+                        cstr_String(fps),
+                        (iMenuItem[]){ { "${dlg.message.ok}", SDLK_RETURN, 0, "message.ok" } },
+                        1);
+                }
+                else {
+                    SDL_SetClipboardText(cstr_String(fps));
+                }
             }
             return iTrue;
         }
@@ -303,7 +312,7 @@ static iBool processEvent_CertListWidget_(iCertListWidget *d, const SDL_Event *e
                 invalidateItem_ListWidget(&d->list, d->contextIndex);
             }
             d->contextIndex = hoverItemIndex_ListWidget(&d->list);
-            updateContextMenu_CertListWidget_(d);                
+            updateContextMenu_CertListWidget_(d);
             /* TODO: Some callback-based mechanism would be nice for updating menus right
                before they open? At least move these to `updateContextMenu_ */
             const iGmIdentity *ident  = constHoverIdentity_CertListWidget(d);
@@ -365,7 +374,7 @@ static void draw_CertItem_(const iCertItem *d, iPaint *p, iRect itemRect,
         bg = uiBackgroundUnfocusedSelection_ColorId;
         fillRect_Paint(p, itemRect, bg);
     }
-//    iInt2 pos = itemRect.pos; 
+//    iInt2 pos = itemRect.pos;
     const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId)
                            : uiTextStrong_ColorId;
     const iBool isUsedOnDomain = (d->indent != 0);
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index d48a2fcd..1a547fa7 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -3154,7 +3154,17 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
     else if (equal_Command(cmd, "server.copycert") && document_App() == d) {
         const iString *fp = collect_String(
             hexEncode_Block(arg_Command(cmd) ? d->certFullFingerprint : d->certFingerprint));
-        SDL_SetClipboardText(cstr_String(fp));
+        if (isTerminal_Platform()) {
+            makeMessage_Widget(
+                arg_Command(cmd) ? "${dlg.cert.fingerprint.full}"
+                                 : "${dlg.cert.fingerprint.pubkey}",
+                cstr_String(fp),
+                (iMenuItem[]){ { "${dlg.message.ok}", SDLK_RETURN, 0, "message.ok" } },
+                1);
+        }
+        else {
+            SDL_SetClipboardText(cstr_String(fp));
+        }
         return iTrue;
     }
     else if (equal_Command(cmd, "copy") && document_App() == d && !focus_Widget()) {
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index 66201119..355486de 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -396,7 +396,7 @@ static iRect contentBounds_InputWidget_(const iInputWidget *d) {
     iRect          bounds = adjusted_Rect(bounds_Widget(w),
                                  addX_I2(padding_(), d->leftPadding),
                                  neg_I2(addX_I2(padding_(), d->rightPadding)));
-    shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0));
+    shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2) * aspect_UI, 0));
     bounds.pos.y += padding_().y / 2;
     if (flags_Widget(w) & extraPadding_WidgetFlag) {
         if (d->sysCtrl && !cmp_String(id_Widget(w), "url")) {
diff --git a/src/ui/root.c b/src/ui/root.c
index ddba9aa2..22289366 100644
--- a/src/ui/root.c
+++ b/src/ui/root.c
@@ -415,6 +415,64 @@ static iBool isBookmarkFolder_(void *context, const iBookmark *bm) {
     return isFolder_Bookmark(bm);
 }
 
+static size_t visibleSize_String(const iString *d) {
+    size_t n = 0;
+    iConstForEach(String, i, d) {
+        if (i.value == '\v') {
+            next_StringConstIterator(&i); /* skip escape */
+            continue;
+        }
+        n += width_Char(i.value);
+    }
+    return n;
+}
+
+static void formatShortcut_(iString *d, int maxLen, const char *label, int key, int mods) {
+    iString str;
+    init_String(&str);
+    iString keyStr;
+    init_String(&keyStr);
+    toString_Sym(key, mods, &keyStr);
+    if (!isEmpty_String(d)) {
+        appendCStr_String(&str, "   ");
+    }
+    appendFormat_String(&str,
+                  "%s%s%s %s",
+                  escape_Color(uiTextShortcut_ColorId),
+                  cstr_String(&keyStr),
+                  uiText_ColorEscape,
+                  label);
+    if (visibleSize_String(d) + visibleSize_String(&str) <= maxLen) {
+        append_String(d, &str);
+    }
+    deinit_String(&keyStr);
+    deinit_String(&str);
+}
+
+static void updateTerminalStatus_(iLabelWidget *term) {
+    const iBinding *bind;
+    iString *str = new_String();
+    const int maxLen = width_Widget(root_Widget(as_Widget(term))) - 2;
+    formatShortcut_(str, maxLen, "${term.url}", SDLK_RETURN, 0);
+    bind = findCommand_Keys("document.linkkeys arg:1");
+    formatShortcut_(str, maxLen, "${term.linkkeys}", bind->key, bind->mods);
+    bind = findCommand_Keys("contextkey");
+    formatShortcut_(str, maxLen, "${term.menu}", bind->key, bind->mods);
+    bind = findCommand_Keys("menubar.focus");
+    formatShortcut_(str, maxLen, "${term.menubar}", bind->key, bind->mods);
+    formatShortcut_(str, maxLen, "${cancel}", 'g', KMOD_CTRL);
+    formatShortcut_(str, maxLen, "${term.focus}", SDLK_TAB, 0);
+    formatShortcut_(str, maxLen, "${term.sidebar}", leftSidebar_KeyShortcut);
+    bind = findCommand_Keys("tabs.new append:1");
+    formatShortcut_(str, maxLen, "${term.tab.new}", bind->key, bind->mods);
+    bind = findCommand_Keys("tabs.close");
+    formatShortcut_(str, maxLen, "${term.tab.close}", bind->key, bind->mods);
+    bind = findCommand_Keys("document.linkkeys arg:1 hover:1");
+    formatShortcut_(str, maxLen, "${term.hover}", bind->key, bind->mods);
+    updateText_LabelWidget(term, str);
+    delete_String(str);
+}
+
 iBool handleRootCommands_Widget(iWidget *root, const char *cmd) {
     iUnused(root);
     if (equal_Command(cmd, "menu.open")) {
@@ -527,9 +585,26 @@ iBool handleRootCommands_Widget(iWidget *root, const char *cmd) {
         if (menubar) {
             setFocus_Widget(child_Widget(menubar, prefs_App()->recentMenuBarIndex));
             postCommand_Widget(focus_Widget(), "trigger");
+            if (isTerminal_Platform()) {
+                setFlags_Widget(findChild_Widget(root, "termstatus"), hidden_WidgetFlag, iTrue);
+            }
         }
         return iTrue;
     }
+    else if (isTerminal_Platform() && equal_Command(cmd, "focus.gained")) {
+        if (!cmp_String(id_Widget(parent_Widget(focus_Widget())), "menubar")) {
+            setFlags_Widget(findChild_Widget(root, "termstatus"), hidden_WidgetFlag, iTrue);
+        }
+        return iFalse;
+    }
+    else if (isTerminal_Platform() && equal_Command(cmd, "menu.closed")) {
+        if (!focus_Widget()) {
+            iLabelWidget *status = findChild_Widget(root, "termstatus");
+            setFlags_Widget(as_Widget(status), hidden_WidgetFlag, iFalse);
+            updateTerminalStatus_(status);
+        }
+        return iFalse;
+    }
     else if (equal_Command(cmd, "input.resized")) {
         /* No parent handled this, so do a full rearrangement. */
         /* TODO: Defer this and do a single rearrangement later. */
@@ -591,6 +666,9 @@ iBool handleRootCommands_Widget(iWidget *root, const char *cmd) {
         return iTrue;
     }
     else if (equal_Command(cmd, "window.resized")) {
+        if (isTerminal_Platform()) {
+            updateTerminalStatus_(findWidget_Root("termstatus"));
+        }
         iSidebarWidget *sidebar = findChild_Widget(root, "sidebar");
         iSidebarWidget *sidebar2 = findChild_Widget(root, "sidebar2");
         if (deviceType_App() != phone_AppDeviceType) {
@@ -889,7 +967,7 @@ static void updateUrlInputContentPadding_(iWidget *navBar) {
     const int indicatorsWidth = width_Widget(findChild_Widget(navBar, "url.rightembed"));
     /* The indicators widget has a padding that covers the urlButtons area. */
     setContentPadding_InputWidget(url,
-                                  lockWidth - 2 * gap_UI, // * 0.75f,
+                                  isTerminal_Platform() ? lockWidth : (lockWidth - 2 * gap_UI),
                                   indicatorsWidth);
 }
 
@@ -1490,6 +1568,8 @@ void updateMetrics_Root(iRoot *d) {
         setFixedSize_Widget(appIcon, init_I2(appIconSize_Root(), appMin->rect.size.y));
     }
     iWidget      *navBar     = findChild_Widget(d->widget, "navbar");
+    iWidget      *menuBar    = findChild_Widget(d->widget, "menubar");
+    iWidget      *termStatus = findChild_Widget(d->widget, "termstatus");
     iWidget      *url        = findChild_Widget(d->widget, "url");
     iWidget      *rightEmbed = findChild_Widget(navBar, "url.rightembed");
     iWidget      *embedPad   = findChild_Widget(navBar, "url.embedpad");
@@ -1505,6 +1585,9 @@ void updateMetrics_Root(iRoot *d) {
     if (navBar) {
         updateUrlInputContentPadding_(navBar);
     }
+    if (termStatus) {
+        setFixedSize_Widget(menuBar, menuBar->rect.size);
+    }
     if (idName) {
         setFixedSize_Widget(as_Widget(idName),
                             init_I2(-1, 2 * gap_UI + lineHeight_Text(uiLabelTiny_FontId)));
@@ -1534,8 +1617,9 @@ static iBool updateWindowMenu_(iWidget *menuBarItem, const char *cmd) {
         /* Remove the old dynamic window list items first. See `windowMenuItems_` in window.c
            for the fixed list. */
         iWidget *menu = findChild_Widget(menuBarItem, "menu");
-        while (childCount_Widget(menu) > 9) {
-            destroy_Widget(removeChild_Widget(menu, child_Widget(menu, 9)));
+        const size_t numFixedItems = numWindowMenuItems_Window() + 1;
+        while (childCount_Widget(menu) > numFixedItems) {
+            destroy_Widget(removeChild_Widget(menu, child_Widget(menu, numFixedItems)));
         }
         iArray winItems;
         init_Array(&winItems, sizeof(iMenuItem));
@@ -1736,16 +1820,19 @@ void createUserInterface_Root(iRoot *d) {
         /* TODO: Use Widget's `updateMenuItems` callback. */
         setCommandHandler_Widget(child_Widget(menuBar, 5), updateWindowMenu_);
         setId_Widget(menuBar, "menubar");
-#  if 0
-        addChildFlags_Widget(menuBar, iClob(new_Widget()), expand_WidgetFlag);
-        /* It's nice to use this space for something, but it should be more valuable than
-           just the app version... */
-        iLabelWidget *ver = addChildFlags_Widget(menuBar, iClob(new_LabelWidget(LAGRANGE_APP_VERSION, NULL)),
-                                                 frameless_WidgetFlag);
-        setTextColor_LabelWidget(ver, uiAnnotation_ColorId);
-#  endif
     }
 #endif
+    /* Terminal status/help indicator. */
+    if (isTerminal_Platform()) {
+        iWidget *termStatus =
+            addChildFlags_Widget(div,
+                                 iClob(new_LabelWidget("", NULL)),
+                                 fixedPosition_WidgetFlag | fixedHeight_WidgetFlag |
+                                 resizeToParentWidth_WidgetFlag);
+        setBackgroundColor_Widget(termStatus, uiBackground_ColorId);
+        updateTerminalStatus_((iLabelWidget *) termStatus);
+        setId_Widget(termStatus, "termstatus");
+    }
     iWidget *navBar;
     /* Navigation bar. */ {
         navBar = new_Widget();
diff --git a/src/ui/util.c b/src/ui/util.c
index 4c2c3bf8..3cacc019 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -2783,10 +2783,14 @@ static iBool messageHandler_(iWidget *msg, const char *cmd) {
           equal_Command(cmd, "theme.changed") ||
           equal_Command(cmd, "focus.lost") ||
           equal_Command(cmd, "focus.gained") ||
+          equal_Command(cmd, "menu.open") ||
+          equal_Command(cmd, "menu.opened") ||
           equal_Command(cmd, "menu.closed") ||
+          startsWith_CStr(cmd, "cancel menu:") ||
           startsWith_CStr(cmd, "feeds.update.") ||
           startsWith_CStr(cmd, "window."))) {
-        //printf("message dismissed by: %s\n", cmd); fflush(stdout);
+        // printf("message dismissed by: %s\n", cmd); fflush(stdout);
+        // SDL_Delay(5000);
         setupSheetTransition_Mobile(msg, dialogTransitionDir_Widget(msg));
         destroy_Widget(msg);
     }
@@ -3994,6 +3998,9 @@ const iArray *makeBookmarkFolderActions_MenuItem(const char *command, iBool with
 }
 
 void enableResizing_Widget(iWidget *d, int minWidth, const char *resizeId) {
+    if (isTerminal_Platform()) {
+        return; /* cannot grab edges */
+    }
     if (deviceType_App() == desktop_AppDeviceType) {
         iChangeFlags(d->flags, arrangeWidth_WidgetFlag, iFalse);
         d->flags2 |= horizontallyResizable_WidgetFlag2;
@@ -4138,7 +4145,7 @@ iWidget *makeBookmarkEditor_Widget(uint32_t folderId, iBool withDup) {
             addDialogToggle_(headings, values, "${bookmark.tag.linksplit}:", "bmed.tag.linksplit");
         }
         arrange_Widget(dlg);
-        const int inputWidth = 100 * gap_UI - headings->rect.size.x;
+        const int inputWidth = iMin(100 * gap_UI, width_Rect(rect_Root(dlg->root))) - headings->rect.size.x;
         for (int i = 0; i < 4; ++i) {
             if (inputs[i]) {
                 as_Widget(inputs[i])->rect.size.x = inputWidth;
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/dev/cdiff/852cc782f198e250ba30f8b3974a39a66cbd1164
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
34.383556 milliseconds
Gemini-to-HTML Time
0.745711 milliseconds

This content has been proxied by September (ba2dc).