Skip to content

shrimple πŸ‡΅πŸ‡± πŸ³οΈβ€βš§οΈ

shrimple mind. shrimple problems. complex solutions. she/her

Slash-hierarchical list names β€” my draft implementation for Offpunk

Posted on January[²⁰26], Thursday 29.January[²⁰26], Friday 30. By Shrimple 2 Comments on Slash-hierarchical list names β€” my draft implementation for Offpunk

More draft patches for Offpunk from me that I haven’t particularly tested much yet.

Offpunk has no support for slash-containing list names β€” it just crashes on an attempt, because the names are neither supported by creating directories nor sanitized to not contain slashes. I wrote a patch for that. And another to make the all lists view, and other logic involving iterating through all lists like subscriptions and syncs, traverse them all, flatly for now.

This was related to imagined usage habit for a concept of subscribe-into-list functionality, where I optionally parameterized the #subscribe status marker as #subscribe(…), so that the parameter, a name of the target list, would get the updated feed lists instead of the tour, and the tour would get a link to that list instead if anything came up from the sync. That other patch is now published: Subscription into list rather than tour β€” Offpunk draft feature patch


From 595dffb5098a7af98d617cca23787a5baa51e27c Mon Sep 17 00:00:00 2001
Date: Thu, 29 Jan 2026 19:34:46 +0100
Subject: [PATCH 1/2] Fix/feature for slash-hierarchical list names

---
 offpunk.py | 40 ++++++++++++++++++++++++++++------------
 1 file changed, 28 insertions(+), 12 deletions(-)

diff --git a/offpunk.py b/offpunk.py
index eeadc42..690682b 100755
--- a/offpunk.py
+++ b/offpunk.py
@@ -1837,24 +1837,40 @@ Use "view XX" where XX is a number to view information about link XX.
             display = not self.sync_only
             self._go_to_url(url, handle=display)
 
+    @staticmethod
+    def _list_path(list_name: str) -> tuple[str | bytes,
+                                            str | bytes,
+                                            bool]:
+        """
+        :param list_name: name of the list, may be hierarchical
+        :return: 1. path of list file, 2. path of list dir, 3. file exists
+        """
+        listdir = os.path.join(xdg("data"), "lists")
+        list_path_components: list[str] = [listdir] + list_name.split("/")
+        list_filename = ("%s.gmi" % list_path_components[-1])
+        list_path_dir = os.path.join(*list_path_components[:-1])
+        list_path = os.path.join(list_path_dir, list_filename)
+        return list_path, list_path_dir, os.path.exists(list_path)
+
     # return the path of the list file if list exists.
     # return None if the list doesn’t exist.
-    def list_path(self, list):
-        listdir = os.path.join(xdg("data"), "lists")
-        list_path = os.path.join(listdir, "%s.gmi" % list)
-        if os.path.exists(list_path):
-            return list_path
-        else:
-            return None
+    @staticmethod
+    def list_path(list_name: str) -> str | bytes | None:
+        """
+        :param list_name:
+        :return:
+            the path of the list file if list exists.
+            None if the list doesn’t exist.
+        """
+        list_path, _, exists = GeminiClient._list_path(list_name)
+        return list_path if exists else None
 
     def list_create(self, list, title=None, quite=False):
-        list_path = self.list_path(list)
+        list_path, dirs, exists = self._list_path(list)
         if list in ["create", "edit", "delete", "help"]:
             print(_("%s is not allowed as a name for a list") % list)
-        elif not list_path:
-            listdir = os.path.join(xdg("data"), "lists")
-            os.makedirs(listdir, exist_ok=True)
-            list_path = os.path.join(listdir, "%s.gmi" % list)
+        elif not exists:
+            os.makedirs(dirs, exist_ok=True)
             with open(list_path, "a") as lfile:
                 if title:
                     lfile.write("# %s\n" % title)
-- 
2.52.0

From 524e12b75c02761faaf816eea1f185eeb023d750 Mon Sep 17 00:00:00 2001
Date: Thu, 29 Jan 2026 23:37:41 +0100
Subject: [PATCH 2/2] Reworked all-lists view to traverse slash-named

---
 ansicat.py  | 40 +++++++++++++++++++++++++++++-----------
 netcache.py |  4 ++--
 offpunk.py  | 34 ++++++++++++++--------------------
 3 files changed, 45 insertions(+), 33 deletions(-)

diff --git a/ansicat.py b/ansicat.py
index 6feba36..74b3abe 100755
--- a/ansicat.py
+++ b/ansicat.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python3
 import argparse
 import base64
+from collections.abc import Sequence, Generator
 import fnmatch
 import html
 import mimetypes
@@ -914,6 +915,26 @@ class GopherRenderer(AbstractRenderer):
                     r.add_text(line)
         return r.get_final(), links
 
+def list_lists(traverse=True,
+               sub: Sequence[str] = tuple(),
+               path_prefixing=True,
+               listdir=os.path.join(xdg("data"), "lists")) \
+        -> Generator[str]:
+    listdir: str | bytes = os.path.join(listdir, *sub)
+    slashname = "".join(i + "/" for i in sub) if path_prefixing else None
+    if os.path.exists(listdir):
+        lists = os.scandir(listdir)
+        for l in lists:
+            # Taking only files with .gmi
+            if l.name.endswith(".gmi"):
+                # removing the .gmi at the end of the name
+                yield slashname + l.name[:-4]
+            elif l.is_dir():
+                if traverse:
+                    yield from list_lists(True, tuple(sub) + (l.name,), path_prefixing)
+                else:
+                    yield slashname + l.name + "/"
+
 
 class FolderRenderer(GemtextRenderer):
     # it was initialized with:
@@ -922,12 +943,16 @@ class FolderRenderer(GemtextRenderer):
         GemtextRenderer.__init__(self, content, url, center)
         self.datadir = datadir
 
+    @property
+    def listdir(self):
+        return os.path.join(self.datadir, "lists")
+
     def get_mime(self):
         return "Directory"
 
     def prepare(self, body, mode=None):
         def get_first_line(l):
-            path = os.path.join(listdir, l + ".gmi")
+            path = os.path.join(self.listdir, l + ".gmi")
             with open(path) as f:
                 first_line = f.readline().strip()
                 f.close()
@@ -947,17 +972,8 @@ class FolderRenderer(GemtextRenderer):
                     body += "=> %s %s (%s items)\n" % (str(path), li, size)
             return body
 
-        listdir = os.path.join(self.datadir, "lists")
         self.title = "My lists"
-        lists = []
-        if os.path.exists(listdir):
-            listfiles = os.listdir(listdir)
-            if len(listfiles) > 0:
-                for l in listfiles:
-                    #We only take gmi files
-                    if l.endswith(".gmi"):
-                        # removing the .gmi at the end of the name
-                        lists.append(l[:-4])
+        lists = list(list_lists(listdir=self.listdir))
         if len(lists) > 0:
             body = ""
             my_lists = []
@@ -990,6 +1006,8 @@ class FolderRenderer(GemtextRenderer):
                 body += _("\n## System Lists\n")
                 body += write_list(system_lists)
             return [[body, None]]
+        else:
+            return [[_("\n## No lists found\n"), None]]
 
 
 class FeedRenderer(GemtextRenderer):
diff --git a/netcache.py b/netcache.py
index c1880f5..dc6533e 100755
--- a/netcache.py
+++ b/netcache.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python3
 import argparse
 import codecs
+from collections.abc import Sequence, Generator
 import datetime
 import getpass
 import glob
@@ -139,7 +140,6 @@ def is_cache_valid(url, validity=0):
         # There’s not even a cache!
         return False
 
-
 def get_cache_path(url, add_index=True):
     # Sometimes, cache_path became a folder! (which happens for index.html/index.gmi)
     # In that case, we need to reconstruct it
@@ -166,7 +166,7 @@ def get_cache_path(url, add_index=True):
         elif url.startswith("list://"):
             listdir = os.path.join(xdg("data"), "lists")
             listname = url[7:].lstrip("/")
-            if listname in [""]:
+            if listname == "":
                 name = "My Lists"
                 path = listdir
             else:
diff --git a/offpunk.py b/offpunk.py
index 690682b..f37a816 100755
--- a/offpunk.py
+++ b/offpunk.py
@@ -9,6 +9,8 @@ __version__ = "2.8"
 # Initial imports and conditional imports {{{
 import argparse
 import cmd
+from collections.abc import Sequence, Generator
+from itertools import chain
 import os
 import os.path
 import shutil
@@ -252,11 +254,11 @@ class GeminiClient(cmd.Cmd):
         # We need to autocomplete listname for the first or second argument
         # If the first one is a cmds
         if words <= 1:
-            allowed = lists + cmds
+            allowed = chain(lists, cmds)
         elif words == 2:
             # if text, the completing word is the second
             if text:
-                allowed = lists + cmds
+                allowed = chain(lists, cmds)
             else:
                 current_cmd = line.split()[1]
                 if current_cmd in ["help", "create"]:
@@ -1565,8 +1567,8 @@ Use "view XX" where XX is a number to view information about link XX.
     def get_list(self, list):
         list_path = self.list_path(list)
         if not list_path:
-            old_file_gmi = os.path.join(xdg("config"), list + ".gmi")
-            old_file_nogmi = os.path.join(xdg("config"), list)
+            old_file_gmi: str = os.path.join(xdg("config"), list + ".gmi")
+            old_file_nogmi: str = os.path.join(xdg("config"), list)
             target = os.path.join(xdg("data"), "lists")
             if os.path.exists(old_file_gmi):
                 shutil.move(old_file_gmi, target)
@@ -1896,8 +1898,7 @@ Use "view XX" where XX is a number to view information about link XX.
             if not list_path:
                 print(_("%s is not a list, aborting the move") % args[0])
             else:
-                lists = self.list_lists()
-                for l in lists:
+                for l in self.list_lists():
                     if l != args[0] and l not in ["archives", "history"]:
                         url = unmode_url(self.current_url)[0]
                         isremoved = self.list_rm_url(url, l)
@@ -1905,18 +1906,12 @@ Use "view XX" where XX is a number to view information about link XX.
                             print(_("Removed from %s") % l)
                 self.list_add_line(args[0])
 
-    def list_lists(self):
-        listdir = os.path.join(xdg("data"), "lists")
-        to_return = []
-        if os.path.exists(listdir):
-            lists = os.listdir(listdir)
-            if len(lists) > 0:
-                for l in lists:
-                    # Taking only files with .gmi
-                    if l.endswith(".gmi"):
-                        # removing the .gmi at the end of the name
-                        to_return.append(l[:-4])
-        return to_return
+    @staticmethod
+    def list_lists(traverse=True,
+                   sub: Sequence[str] = tuple(),
+                   path_prefixing=True)\
+            -> Generator[str]:
+        return ansicat.list_lists(traverse, sub, path_prefixing)
 
     def list_has_status(self, list, status):
         path = self.list_path(list)
@@ -1991,8 +1986,7 @@ Use "view XX" where XX is a number to view information about link XX.
         listdir = os.path.join(xdg("data"), "lists")
         os.makedirs(listdir, exist_ok=True)
         if not arg:
-            lists = self.list_lists()
-            if len(lists) > 0:
+            if next(self.list_lists()):
                 lurl = "list:///"
                 self._go_to_url(lurl)
             else:
-- 
2.52.0
Wild Software Writing Tags:big.ugly.git.patch., offpunk, offpunk:lists, oss-contributing, subscribe

Post navigation

Previous Post: Bugfix for list URI for my Offpunk redirections implementation draft
Next Post: Subscription into list rather than tour β€” Offpunk draft feature patch

Related Posts

  • Distributed file version management in 15 minutes of Bash Wild Software Writing
  • Simplistic reconciliation of mostly-append text files like Offpunk lists: draft involving Kahn’s algorithm Wild Software Writing
  • Amending my Offpunk redirection implementation Wild Software Writing
  • Subscription into list rather than tour β€” Offpunk draft feature patch Wild Software Writing
  • Experimentally expanding Offpunk browser Part 1 (nightly) Wild Software Writing
  • Links 2, a graphical browser I wanna build upon. And a quick look at how ELinks is doing. Wild Software Writing

Comments (2) on “Slash-hierarchical list names β€” my draft implementation for Offpunk”

  1. Pingback: Subscription into list rather than tour β€” Offpunk draft feature patch – shrimple πŸ‡΅πŸ‡± πŸ³οΈβ€βš§οΈ
  2. Pingback: Bugfix for list URI for my Offpunk redirections implementation draft – shrimple πŸ‡΅πŸ‡± πŸ³οΈβ€βš§οΈ

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Atom feed for this page

Atom feed for this blog

against-messy-software bash big.ugly.git.patch. chromium-and-derivatives community fragment golang kde links2 linux microsoft-edge network offpunk offpunk:lists offpunk:redirections oss-contributing perl programming-tips scripting smolweb subscribe superuser window-decorations Wordpress_ActivityPub_plugin

  • February 2026 (4)
  • January 2026 (10)

Categories

  • Influencing Society

    (1)
  • Meta

    (2)
  • Oddities of alternate reality

    (1)
  • Programming Technologies

    (1)
  • Software Imposed On Us

    (1)
  • Wild Software Writing

    (8)
shrimple πŸ‡΅πŸ‡±  πŸ³οΈβ€βš§οΈ
shrimple πŸ‡΅πŸ‡± πŸ³οΈβ€βš§οΈ
@shrimple@www.shrimple.pl
Follow

shrimple mind. shrimple problems. complex solutions. she/her

14 posts
5 followers

Follow shrimple πŸ‡΅πŸ‡± πŸ³οΈβ€βš§οΈ

My Profile

Copy and paste my profile into the search field of your favorite fediverse app or server.

Your Profile

Or, if you know your own profile, we can start things that way!
  • Hello world! Meta
  • Links 2, a graphical browser I wanna build upon. And a quick look at how ELinks is doing. Wild Software Writing
  • Forcing KWin decorations and MS Edge’s 1cm shadow gradient Software Imposed On Us
  • Why follow requests here and can I even be followed Meta
  • Amending my Offpunk redirection implementation Wild Software Writing
  • Getting TLS1.3 Key Log from Go application with requests by a library, and using it in Wireshark Programming Technologies
  • What if we organized a different kind of hackathon Influencing Society
  • Bugfix for list URI for my Offpunk redirections implementation draft Wild Software Writing

shrimple@shrimple.pl

Copyright © 2026 shrimple πŸ‡΅πŸ‡± πŸ³οΈβ€βš§οΈ.

Powered by PressBook News WordPress theme