sfeed_gopher: improve output of the index file - sfeed - RSS and Atom parser
 (HTM) git clone git://git.codemadness.org/sfeed
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit 67179090976e460a85c14ce701c0c0f0dd8af2fd
 (DIR) parent 86476894e7cc6e41856303957889cf248f4a28a2
 (HTM) Author: Hiltjo Posthuma <hiltjo@codemadness.org>
       Date:   Tue, 11 Nov 2025 20:47:26 +0100
       
       sfeed_gopher: improve output of the index file
       
       - Prefix feeds with new items with "N " at the start of the line.
       - Right align item counts, similar to the (left) feed sidebar in sfeed_curses.
       - Move the colw() function which is used to count the column width in
         sfeed_curses to util.
       
       Diffstat:
         M sfeed_curses.c                      |      31 -------------------------------
         M sfeed_gopher.1                      |       4 +++-
         M sfeed_gopher.c                      |      74 +++++++++++++++++++++++--------
         M util.c                              |      31 +++++++++++++++++++++++++++++++
         M util.h                              |       1 +
       
       5 files changed, 90 insertions(+), 51 deletions(-)
       ---
 (DIR) diff --git a/sfeed_curses.c b/sfeed_curses.c
       @@ -275,37 +275,6 @@ tparmnull(const char *str, long p1, long p2, long p3, long p4, long p5, long p6,
                return tparm((char *)str, p1, p2, p3, p4, p5, p6, p7, p8, p9);
        }
        
       -/* Counts column width of character string. */
       -static size_t
       -colw(const char *s)
       -{
       -        wchar_t wc;
       -        size_t col = 0, i, slen;
       -        int inc, rl, w;
       -
       -        slen = strlen(s);
       -        for (i = 0; i < slen; i += inc) {
       -                inc = 1; /* next byte */
       -                if ((unsigned char)s[i] < 32) {
       -                        continue;
       -                } else if ((unsigned char)s[i] >= 127) {
       -                        rl = mbtowc(&wc, &s[i], slen - i < 4 ? slen - i : 4);
       -                        inc = rl;
       -                        if (rl < 0) {
       -                                mbtowc(NULL, NULL, 0); /* reset state */
       -                                inc = 1; /* invalid, seek next byte */
       -                                w = 1; /* replacement char is one width */
       -                        } else if ((w = wcwidth(wc)) == -1) {
       -                                continue;
       -                        }
       -                        col += w;
       -                } else {
       -                        col++;
       -                }
       -        }
       -        return col;
       -}
       -
        /* Format `len` columns of characters. If string is shorter pad the rest
         * with characters `pad`. */
        static int
 (DIR) diff --git a/sfeed_gopher.1 b/sfeed_gopher.1
       @@ -1,4 +1,4 @@
       -.Dd May 14, 2022
       +.Dd September 21, 2025
        .Dt SFEED_GOPHER 1
        .Os
        .Sh NAME
       @@ -34,6 +34,8 @@ Items with a timestamp from the last day compared to the system time at the
        time of formatting are counted and marked as new.
        This value might be overridden through environment variables.
        Items are marked as new with the prefix "N" at the start of the line.
       +If an index file is used then feeds with new items are marked as new with the
       +prefix "N" at the start of the line.
        .Sh ENVIRONMENT
        .Bl -tag -width Ds
        .It Ev SFEED_GOPHER_PATH
 (DIR) diff --git a/sfeed_gopher.c b/sfeed_gopher.c
       @@ -1,3 +1,4 @@
       +#include <locale.h>
        #include <stdio.h>
        #include <stdlib.h>
        #include <string.h>
       @@ -9,6 +10,7 @@ static char *prefixpath = "/", *host = "127.0.0.1", *port = "70"; /* default */
        static char *line;
        static size_t linesize;
        static time_t comparetime;
       +static struct feed *feeds;
        
        /* Escape characters in Gopher, CR and LF are ignored */
        static void
       @@ -123,12 +125,13 @@ printfeed(FILE *fpitems, FILE *fpin, struct feed *f)
        int
        main(int argc, char *argv[])
        {
       -        struct feed f = { 0 };
       +        struct feed *f;
                FILE *fpitems, *fpindex, *fp;
       -        char *name, *p;
       +        char buf[64], *name, *p;
       +        size_t maxcountlen = 0, maxnamelen = 0, len;
                int i;
        
       -        if (argc == 1) {
       +        if (argc <= 1) {
                        if (pledge("stdio", NULL) == -1)
                                err(1, "pledge");
                } else {
       @@ -138,6 +141,8 @@ main(int argc, char *argv[])
                                err(1, "unveil: .");
                        if (pledge("stdio rpath wpath cpath", NULL) == -1)
                                err(1, "pledge");
       +
       +                setlocale(LC_CTYPE, "");
                }
        
                if ((comparetime = getcomparetime()) == (time_t)-1)
       @@ -147,41 +152,72 @@ main(int argc, char *argv[])
                        host = p;
                if ((p = getenv("SFEED_GOPHER_PORT")))
                        port = p;
       +        if ((p = getenv("SFEED_GOPHER_PATH")))
       +                prefixpath = p;
       +
       +        if (!(feeds = calloc(argc <= 1 ? 1 : argc, sizeof(struct feed))))
       +                err(1, "calloc");
        
       -        if (argc == 1) {
       -                f.name = "";
       -                printfeed(stdout, stdin, &f);
       +        if (argc <= 1) {
       +                feeds[0].name = "";
       +                printfeed(stdout, stdin, &feeds[0]);
                        checkfileerror(stdin, "<stdin>", 'r');
                        checkfileerror(stdout, "<stdout>", 'w');
                } else {
       -                if ((p = getenv("SFEED_GOPHER_PATH")))
       -                        prefixpath = p;
       -
       -                /* write main index page */
       -                if (!(fpindex = fopen("index", "wb")))
       -                        err(1, "fopen: index");
       -
                        for (i = 1; i < argc; i++) {
       -                        memset(&f, 0, sizeof(f));
                                name = ((name = strrchr(argv[i], '/'))) ? name + 1 : argv[i];
       -                        f.name = name;
       +                        f = &feeds[i - 1];
       +                        f->name = name;
        
                                if (!(fp = fopen(argv[i], "r")))
                                        err(1, "fopen: %s", argv[i]);
                                if (!(fpitems = fopen(name, "wb")))
                                        err(1, "fopen");
       -                        printfeed(fpitems, fp, &f);
       +                        printfeed(fpitems, fp, f);
                                checkfileerror(fp, argv[i], 'r');
                                checkfileerror(fpitems, name, 'w');
                                fclose(fp);
                                fclose(fpitems);
        
       +                        /* count max length: used for aligning the feed names */
       +                        len = colw(name);
       +                        if (len > maxnamelen)
       +                                maxnamelen = len;
       +
       +                        /* count max length: used for aligning the feed counts to the right */
       +                        len = snprintf(NULL, 0, " (%lu/%lu)", f->totalnew, f->total);
       +                        if (len > maxcountlen)
       +                                maxcountlen = len;
       +                }
       +        }
       +
       +        /* write index file */
       +        if (argc > 1) {
       +                if (!(fpindex = fopen("index", "wb")))
       +                        err(1, "fopen: index");
       +
       +                for (i = 0; i < argc - 1; i++) {
       +                        f = &feeds[i];
       +
                                /* append directory item to index */
                                fputs("1", fpindex);
       -                        gophertext(fpindex, name);
       -                        fprintf(fpindex, " (%lu/%lu)\t", f.totalnew, f.total);
       +                        fputs(f->totalnew ? "N " : "  ", fpindex);
       +
       +                        /* left align feed names and pad with spaces */
       +                        len = colw(f->name);
       +                        gophertext(fpindex, f->name);
       +                        for (; len < maxnamelen; len++)
       +                                fputs(" ", fpindex);
       +
       +                        /* right align the item counts by padding with spaces */
       +                        snprintf(buf, sizeof(buf), " (%lu/%lu)", f->totalnew, f->total);
       +                        len = strlen(buf);
       +                        for (; len < maxcountlen; len++)
       +                                fputs(" ", fpindex);
       +                        fprintf(fpindex, "%s\t", buf);
       +
                                gophertext(fpindex, prefixpath);
       -                        gophertext(fpindex, name);
       +                        gophertext(fpindex, f->name);
                                fprintf(fpindex, "\t%s\t%s\r\n", host, port);
                        }
                        fputs(".\r\n", fpindex);
 (DIR) diff --git a/util.c b/util.c
       @@ -406,3 +406,34 @@ printutf8pad(FILE *fp, const char *s, size_t len, int pad)
                for (; col < len; ++col)
                        putc(pad, fp);
        }
       +
       +/* Counts column width of a character string. */
       +size_t
       +colw(const char *s)
       +{
       +        wchar_t wc;
       +        size_t col = 0, i, slen;
       +        int inc, rl, w;
       +
       +        slen = strlen(s);
       +        for (i = 0; i < slen; i += inc) {
       +                inc = 1; /* next byte */
       +                if ((unsigned char)s[i] < 32) {
       +                        continue;
       +                } else if ((unsigned char)s[i] >= 127) {
       +                        rl = mbtowc(&wc, &s[i], slen - i < 4 ? slen - i : 4);
       +                        inc = rl;
       +                        if (rl < 0) {
       +                                mbtowc(NULL, NULL, 0); /* reset state */
       +                                inc = 1; /* invalid, seek next byte */
       +                                w = 1; /* replacement char is one width */
       +                        } else if ((w = wcwidth(wc)) == -1) {
       +                                continue;
       +                        }
       +                        col += w;
       +                } else {
       +                        col++;
       +                }
       +        }
       +        return col;
       +}
 (DIR) diff --git a/util.h b/util.h
       @@ -72,6 +72,7 @@ int uri_makeabs(struct uri *, struct uri *, struct uri *);
        int uri_parse(const char *, struct uri *);
        
        void checkfileerror(FILE *, const char *, int);
       +size_t colw(const char *);
        time_t getcomparetime(void);
        void parseline(char *, char *[FieldLast]);
        void printutf8pad(FILE *, const char *, size_t, int);