sfeed

simple feed reader - forked from git.codemadness.org/sfeed
git clone git://src.gearsix.net/sfeed
Log | Files | Refs | Atom | README | LICENSE

commit dc7f31a8a2f4a0adf60f6e228688fbe2ab31b42c
parent 5062ab20feb901630dc69e8ed49d05d82b1756ba
Author: gearsix <gearsix@tuta.io>
Date:   Sun,  5 Mar 2023 12:07:48 +0000

Merge branch 'master' into gearsix

Diffstat:
MLICENSE | 2+-
MMakefile | 2+-
MREADME | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
MREADME.xml | 4++--
Mminicurses.h | 2--
Msfeed.1 | 16+++++++++++++---
Msfeed.5 | 9+++------
Msfeed.c | 63+++++++++++++++++++++++++++++++--------------------------------
Msfeed_curses.1 | 69++++++++++++++++++++++++++++++++++++++++++---------------------------
Msfeed_curses.c | 245+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msfeed_gopher.1 | 6+++---
Msfeed_gopher.c | 17++++++-----------
Msfeed_markread | 6+++---
Msfeed_mbox.c | 4++--
Msfeed_opml_export | 14+++++++++-----
Msfeed_opml_import.c | 3+--
Msfeed_plain.1 | 4++--
Msfeed_update | 12++++++------
Msfeed_update.1 | 11++++++-----
Msfeed_web.c | 3+--
Msfeed_xmlenc.c | 7+++----
Msfeedrc.5 | 25++++++++++++++-----------
Mthemes/mono.h | 8++++----
Mthemes/mono_highlight.h | 8++++----
Mthemes/newsboat.h | 8++++----
Mutil.c | 9++++-----
Mutil.h | 11+++++++++--
Mxml.c | 22++++++++++++----------
Mxml.h | 4++--
29 files changed, 491 insertions(+), 275 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -1,6 +1,6 @@ ISC License -Copyright (c) 2011-2022 Hiltjo Posthuma <hiltjo@codemadness.org> +Copyright (c) 2011-2023 Hiltjo Posthuma <hiltjo@codemadness.org> Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/Makefile b/Makefile @@ -1,7 +1,7 @@ .POSIX: NAME = sfeed -VERSION = 1.3 +VERSION = 1.7 # curses theme, see themes/ directory. SFEED_THEME = mono diff --git a/README b/README @@ -86,6 +86,7 @@ HTML view (no frames), copy style.css for a default style: HTML view with the menu as frames, copy style.css for a default style: mkdir -p "$HOME/.sfeed/frames" + cp style.css "$HOME/.sfeed/frames/style.css" cd "$HOME/.sfeed/frames" && sfeed_frames $HOME/.sfeed/feeds/* To automatically update your feeds periodically and format them in a way you @@ -155,8 +156,8 @@ sfeed supports a subset of XML 1.0 and a subset of: - MediaRSS extensions (media:). - Dublin Core extensions (dc:). -Other formats like JSONfeed, twtxt or certain RSS/Atom extensions can be -supported by converting them to RSS/Atom or to the sfeed(5) format directly. +Other formats like JSONfeed, twtxt or certain RSS/Atom extensions are supported +by converting them to RSS/Atom or to the sfeed(5) format directly. OS tested @@ -260,8 +261,8 @@ output example: - - - -Make sure your sfeedrc config file exists, see sfeedrc.example. To update your -feeds (configfile argument is optional): +Make sure your sfeedrc config file exists, see the sfeedrc.example file. To +update your feeds (configfile argument is optional): sfeed_update "configfile" @@ -588,7 +589,7 @@ procmail_maildirs.sh file: mkdir -p "${maildir}/.cache" if ! test -r "${procmailconfig}"; then - echo "Procmail configuration file \"${procmailconfig}\" does not exist or is not readable." >&2 + printf "Procmail configuration file \"%s\" does not exist or is not readable.\n" "${procmailconfig}" >&2 echo "See procmailrc.example for an example." >&2 exit 1 fi @@ -843,7 +844,7 @@ arguments are specified then the data is read from stdin. if [ "${SFEED_DOWNLOAD_CHILD}" = "1" ]; then # Downloader helper for parallel downloading. # Receives arguments: $1 = URL, $2 = title, $3 = feed filename or "-". - # It should write the URI to the cachefile if it is succesful. + # It should write the URI to the cachefile if it is successful. downloader "$1" "$2" "$3" exit $? fi @@ -980,6 +981,53 @@ TSV format. - - - +Progress indicator +------------------ + +The below sfeed_update wrapper script counts the amount of feeds in a sfeedrc +config. It then calls sfeed_update and pipes the output lines to a function +that counts the current progress. It writes the total progress to stderr. +Alternative: pv -l -s totallines + + #!/bin/sh + # Progress indicator script. + + # Pass lines as input to stdin and write progress status to stderr. + # progress(totallines) + progress() { + total="$(($1 + 0))" # must be a number, no divide by zero. + test "${total}" -le 0 -o "$1" != "${total}" && return + LC_ALL=C awk -v "total=${total}" ' + { + counter++; + percent = (counter * 100) / total; + printf("\033[K") > "/dev/stderr"; # clear EOL + print $0; + printf("[%s/%s] %.0f%%\r", counter, total, percent) > "/dev/stderr"; + fflush(); # flush all buffers per line. + } + END { + printf("\033[K") > "/dev/stderr"; + }' + } + + # Counts the feeds from the sfeedrc config. + countfeeds() { + count=0 + . "$1" + feed() { + count=$((count + 1)) + } + feeds + echo "${count}" + } + + config="${1:-$HOME/.sfeed/sfeedrc}" + total=$(countfeeds "${config}") + sfeed_update "${config}" 2>&1 | progress "${total}" + +- - - + Counting unread and total items ------------------------------- @@ -1001,7 +1049,7 @@ formatting tools do: END { print "New: " totalnew; print "Total: " total; - }' ~/.sfeed/urls ~/.sfeed/feeds/* + }' ~/.sfeed/feeds/* The below example script counts the unread items using the sfeed_curses URL file: @@ -1032,6 +1080,98 @@ file: - - - +sfeed.c: adding new XML tags or sfeed(5) fields to the parser +------------------------------------------------------------- + +sfeed.c contains definitions to parse XML tags and map them to sfeed(5) TSV +fields. Parsed RSS and Atom tag names are first stored as a TagId, which is a +number. This TagId is then mapped to the output field index. + +Steps to modify the code: + +* Add a new TagId enum for the tag. + +* (optional) Add a new FeedField* enum for the new output field or you can map + it to an existing field. + +* Add the new XML tag name to the array variable of parsed RSS or Atom + tags: rsstags[] or atomtags[]. + + These must be defined in alphabetical order, because a binary search is used + which uses the strcasecmp() function. + +* Add the parsed TagId to the output field in the array variable fieldmap[]. + + When another tag is also mapped to the same output field then the tag with + the highest TagId number value overrides the mapped field: the order is from + least important to high. + +* If this defined tag is just using the inner data of the XML tag, then this + definition is enough. If it for example has to parse a certain attribute you + have to add a check for the TagId to the xmlattr() callback function. + +* (optional) Print the new field in the printfields() function. + +Below is a patch example to add the MRSS "media:content" tag as a new field: + +diff --git a/sfeed.c b/sfeed.c +--- a/sfeed.c ++++ b/sfeed.c +@@ -50,7 +50,7 @@ enum TagId { + RSSTagGuidPermalinkTrue, + /* must be defined after GUID, because it can be a link (isPermaLink) */ + RSSTagLink, +- RSSTagEnclosure, ++ RSSTagMediaContent, RSSTagEnclosure, + RSSTagAuthor, RSSTagDccreator, + RSSTagCategory, + /* Atom */ +@@ -81,7 +81,7 @@ typedef struct field { + enum { + FeedFieldTime = 0, FeedFieldTitle, FeedFieldLink, FeedFieldContent, + FeedFieldId, FeedFieldAuthor, FeedFieldEnclosure, FeedFieldCategory, +- FeedFieldLast ++ FeedFieldMediaContent, FeedFieldLast + }; + + typedef struct feedcontext { +@@ -137,6 +137,7 @@ static const FeedTag rsstags[] = { + { STRP("enclosure"), RSSTagEnclosure }, + { STRP("guid"), RSSTagGuid }, + { STRP("link"), RSSTagLink }, ++ { STRP("media:content"), RSSTagMediaContent }, + { STRP("media:description"), RSSTagMediaDescription }, + { STRP("pubdate"), RSSTagPubdate }, + { STRP("title"), RSSTagTitle } +@@ -180,6 +181,7 @@ static const int fieldmap[TagLast] = { + [RSSTagGuidPermalinkFalse] = FeedFieldId, + [RSSTagGuidPermalinkTrue] = FeedFieldId, /* special-case: both a link and an id */ + [RSSTagLink] = FeedFieldLink, ++ [RSSTagMediaContent] = FeedFieldMediaContent, + [RSSTagEnclosure] = FeedFieldEnclosure, + [RSSTagAuthor] = FeedFieldAuthor, + [RSSTagDccreator] = FeedFieldAuthor, +@@ -677,6 +679,8 @@ printfields(void) + string_print_uri(&ctx.fields[FeedFieldEnclosure].str); + putchar(FieldSeparator); + string_print_trimmed_multi(&ctx.fields[FeedFieldCategory].str); ++ putchar(FieldSeparator); ++ string_print_trimmed(&ctx.fields[FeedFieldMediaContent].str); + putchar('\n'); + + if (ferror(stdout)) /* check for errors but do not flush */ +@@ -718,7 +722,7 @@ xmlattr(XMLParser *p, const char *t, size_t tl, const char *n, size_t nl, + } + + if (ctx.feedtype == FeedTypeRSS) { +- if (ctx.tag.id == RSSTagEnclosure && ++ if ((ctx.tag.id == RSSTagEnclosure || ctx.tag.id == RSSTagMediaContent) && + isattr(n, nl, STRP("url"))) { + string_append(&tmpstr, v, vl); + } else if (ctx.tag.id == RSSTagGuid && + +- - - + Running custom commands inside the sfeed_curses program ------------------------------------------------------- @@ -1067,7 +1207,23 @@ Example of a `markallread.sh` shellscript to mark all URLs as read: Example of a `syncnews.sh` shellscript to update the feeds and reload them: #!/bin/sh - sfeed_update && pkill -SIGHUP sfeed_curses + sfeed_update + pkill -SIGHUP sfeed_curses + + +Running programs in a new session +--------------------------------- + +By default processes are spawned in the same session and process group as +sfeed_curses. When sfeed_curses is closed this can also close the spawned +process in some cases. + +When the setsid command-line program is available the following wrapper command +can be used to run the program in a new session, for a plumb program: + + setsid -f xdg-open "$@" + +Alternatively the code can be changed to call setsid() before execvp(). Open an URL directly in the same terminal diff --git a/README.xml b/README.xml @@ -28,7 +28,7 @@ Supports - Tags in short-form (<img src="lolcat.jpg" title="Meow" />). - Tag attributes. -- Short attributes without an explicity set value (<input type="checkbox" checked />). +- Short attributes without an explicitly set value (<input type="checkbox" checked />). - Comments - CDATA sections. - Helper function (xml_entitytostr) to convert XML 1.0 / HTML 2.0 named entities @@ -55,7 +55,7 @@ Caveats - The XML specification has no limits on tag and attribute names. For simplicity/sanity sake this XML parser takes some liberties. Tag and attribute names are truncated if they are excessively long. -- Entity expansions are not parsed aswell as DOCTYPE, ATTLIST etc. +- Entity expansions are not parsed as well as DOCTYPE, ATTLIST etc. Files used diff --git a/minicurses.h b/minicurses.h @@ -1,5 +1,3 @@ -#include <sys/ioctl.h> - #undef OK #define OK (0) diff --git a/sfeed.1 b/sfeed.1 @@ -1,4 +1,4 @@ -.Dd November 26, 2021 +.Dd January 7, 2023 .Dt SFEED 1 .Os .Sh NAME @@ -23,8 +23,8 @@ SPACE character. Control characters are removed. .Pp The content field can contain newlines and these are escaped. -TABs, newlines and '\\' are escaped with '\\', so it becomes: '\\t', '\\n' -and '\\\\'. +TABs, newlines and '\e' are escaped with '\e', so it becomes: '\et', '\en' +and '\e\e'. Other whitespace characters except spaces are removed. Control characters are removed. .Pp @@ -55,6 +55,15 @@ Item, categories, multiple values are separated by the '|' character. .Bd -literal curl -s 'https://codemadness.org/atom.xml' | sfeed .Ed +.Pp +To convert the character set from a feed that is not UTF-8 encoded the +.Xr iconv 1 +tool can be used: +.Bd -literal +curl -s 'https://codemadness.org/some_iso-8859-1_feed.xml' | \e +iconv -f iso-8859-1 -t utf-8 | \e +sfeed +.Ed .Sh EXAMPLE SETUP 1. Create a directory for the sfeedrc configuration and the feeds: .Bd -literal @@ -94,6 +103,7 @@ There are also other formatting programs included. The README file has more examples. .Sh SEE ALSO .Xr sfeed_curses 1 , +.Xr sfeed_opml_import 1 , .Xr sfeed_plain 1 , .Xr sfeed_update 1 , .Xr sfeed 5 , diff --git a/sfeed.5 b/sfeed.5 @@ -1,4 +1,4 @@ -.Dd November 23, 2021 +.Dd January 7, 2023 .Dt SFEED 5 .Os .Sh NAME @@ -17,8 +17,8 @@ SPACE character. Control characters are removed. .Pp The content field can contain newlines and these are escaped. -TABs, newlines and '\\' are escaped with '\\', so it becomes: '\\t', '\\n' -and '\\\\'. +TABs, newlines and '\e' are escaped with '\e', so it becomes: '\et', '\en' +and '\e\e'. Other whitespace characters except spaces are removed. Control characters are removed. .Pp @@ -48,6 +48,3 @@ Item, categories, multiple values are separated by the '|' character. .Xr sfeed_plain 1 .Sh AUTHORS .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org -.Sh CAVEATS -If a timezone for the timestamp field is not in the RFC822 or RFC3339 format it -is not supported and the timezone is interpreted as UTC+0. diff --git a/sfeed.c b/sfeed.c @@ -1,4 +1,3 @@ -#include <ctype.h> #include <errno.h> #include <stdint.h> #include <stdio.h> @@ -246,7 +245,7 @@ gettag(enum FeedType feedtype, const char *name, size_t namelen) static char * ltrim(const char *s) { - for (; isspace((unsigned char)*s); s++) + for (; ISSPACE((unsigned char)*s); s++) ; return (char *)s; } @@ -256,7 +255,7 @@ rtrim(const char *s) { const char *e; - for (e = s + strlen(s); e > s && isspace((unsigned char)*(e - 1)); e--) + for (e = s + strlen(s); e > s && ISSPACE((unsigned char)*(e - 1)); e--) ; return (char *)e; } @@ -326,7 +325,7 @@ string_print_encoded(String *s) case '\t': putchar('\\'); putchar('t'); break; default: /* ignore control chars */ - if (!iscntrl((unsigned char)*p)) + if (!ISCNTRL((unsigned char)*p)) putchar(*p); break; } @@ -341,9 +340,9 @@ printtrimmed(const char *s) p = ltrim(s); e = rtrim(p); for (; *p && p != e; p++) { - if (isspace((unsigned char)*p)) + if (ISSPACE((unsigned char)*p)) putchar(' '); /* any whitespace to space */ - else if (!iscntrl((unsigned char)*p)) + else if (!ISCNTRL((unsigned char)*p)) /* ignore other control chars */ putchar(*p); } @@ -514,24 +513,24 @@ gettzoffset(const char *s) long tzhour = 0, tzmin = 0; size_t i; - for (; isspace((unsigned char)*s); s++) + for (; ISSPACE((unsigned char)*s); s++) ; switch (*s) { case '-': /* offset */ case '+': - for (i = 0, p = s + 1; i < 2 && isdigit((unsigned char)*p); i++, p++) + for (i = 0, p = s + 1; i < 2 && ISDIGIT((unsigned char)*p); i++, p++) tzhour = (tzhour * 10) + (*p - '0'); if (*p == ':') p++; - for (i = 0; i < 2 && isdigit((unsigned char)*p); i++, p++) + for (i = 0; i < 2 && ISDIGIT((unsigned char)*p); i++, p++) tzmin = (tzmin * 10) + (*p - '0'); return ((tzhour * 3600) + (tzmin * 60)) * (s[0] == '-' ? -1 : 1); default: /* timezone name */ - for (i = 0; isalpha((unsigned char)s[i]); i++) + for (i = 0; ISALPHA((unsigned char)s[i]); i++) ; if (i != 3) return 0; - /* compare tz and adjust offset relative to UTC */ + /* compare timezone and adjust offset relative to UTC */ for (i = 0; i < sizeof(tzones) / sizeof(*tzones); i++) { if (!memcmp(s, tzones[i].name, 3)) return tzones[i].offhour; @@ -565,35 +564,35 @@ parsetime(const char *s, long long *tp) int va[6] = { 0 }, i, j, v, vi; size_t m; - for (; isspace((unsigned char)*s); s++) + for (; ISSPACE((unsigned char)*s); s++) ; - if (!isdigit((unsigned char)*s) && !isalpha((unsigned char)*s)) + if (!ISDIGIT((unsigned char)*s) && !ISALPHA((unsigned char)*s)) return -1; - if (isdigit((unsigned char)s[0]) && - isdigit((unsigned char)s[1]) && - isdigit((unsigned char)s[2]) && - isdigit((unsigned char)s[3])) { + if (ISDIGIT((unsigned char)s[0]) && + ISDIGIT((unsigned char)s[1]) && + ISDIGIT((unsigned char)s[2]) && + ISDIGIT((unsigned char)s[3])) { /* formats "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S" or "%Y%m%d%H%M%S" */ vi = 0; } else { /* format: "[%a, ]%d %b %Y %H:%M:%S" */ /* parse "[%a, ]%d %b %Y " part, then use time parsing as above */ - for (; isalpha((unsigned char)*s); s++) + for (; ISALPHA((unsigned char)*s); s++) ; - for (; isspace((unsigned char)*s); s++) + for (; ISSPACE((unsigned char)*s); s++) ; if (*s == ',') s++; - for (; isspace((unsigned char)*s); s++) + for (; ISSPACE((unsigned char)*s); s++) ; - for (v = 0, i = 0; i < 2 && isdigit((unsigned char)*s); s++, i++) + for (v = 0, i = 0; i < 2 && ISDIGIT((unsigned char)*s); s++, i++) v = (v * 10) + (*s - '0'); va[2] = v; /* day */ - for (; isspace((unsigned char)*s); s++) + for (; ISSPACE((unsigned char)*s); s++) ; /* end of word month */ - for (j = 0; isalpha((unsigned char)s[j]); j++) + for (j = 0; ISALPHA((unsigned char)s[j]); j++) ; /* check month name */ if (j < 3 || j > 9) @@ -609,15 +608,15 @@ parsetime(const char *s, long long *tp) } if (m >= 12) return -1; /* no month found */ - for (; isspace((unsigned char)*s); s++) + for (; ISSPACE((unsigned char)*s); s++) ; - for (v = 0, i = 0; i < 4 && isdigit((unsigned char)*s); s++, i++) + for (v = 0, i = 0; i < 4 && ISDIGIT((unsigned char)*s); s++, i++) v = (v * 10) + (*s - '0'); /* obsolete short year: RFC2822 4.3 */ if (i <= 3) v += (v >= 0 && v <= 49) ? 2000 : 1900; va[0] = v; /* year */ - for (; isspace((unsigned char)*s); s++) + for (; ISSPACE((unsigned char)*s); s++) ; /* parse only regular time part, see below */ vi = 3; @@ -626,20 +625,20 @@ parsetime(const char *s, long long *tp) /* parse time parts (and possibly remaining date parts) */ for (; *s && vi < 6; vi++) { for (i = 0, v = 0; i < ((vi == 0) ? 4 : 2) && - isdigit((unsigned char)*s); s++, i++) { + ISDIGIT((unsigned char)*s); s++, i++) { v = (v * 10) + (*s - '0'); } va[vi] = v; if ((vi < 2 && *s == '-') || - (vi == 2 && (*s == 'T' || isspace((unsigned char)*s))) || + (vi == 2 && (*s == 'T' || ISSPACE((unsigned char)*s))) || (vi > 2 && *s == ':')) s++; } /* skip milliseconds in for example: "%Y-%m-%dT%H:%M:%S.000Z" */ if (*s == '.') { - for (s++; isdigit((unsigned char)*s); s++) + for (s++; ISDIGIT((unsigned char)*s); s++) ; } @@ -758,7 +757,7 @@ xmlattrentity(XMLParser *p, const char *t, size_t tl, const char *n, size_t nl, return; /* try to translate entity, else just pass as data to - * xmldata handler. */ + * xmlattr handler. */ if ((len = xml_entitytostr(data, buf, sizeof(buf))) > 0) xmlattr(p, t, tl, n, nl, buf, (size_t)len); else @@ -967,7 +966,7 @@ xmltagend(XMLParser *p, const char *t, size_t tl, int isshort) return; if (ISINCONTENT(ctx)) { - /* not close content field */ + /* not a closed content field */ if (!istag(ctx.tag.name, ctx.tag.len, t, tl)) { if (!isshort && ctx.contenttype == ContentTypeHTML) { xmldata(p, "</", 2); @@ -979,7 +978,7 @@ xmltagend(XMLParser *p, const char *t, size_t tl, int isshort) } else if (ctx.tag.id && istag(ctx.tag.name, ctx.tag.len, t, tl)) { /* matched tag end: close it */ /* copy also to the link field if the attribute isPermaLink="true" - and it is not set by a tag with higher prio. */ + and it is not set by a tag with higher priority. */ if (ctx.tag.id == RSSTagGuidPermalinkTrue && ctx.field && ctx.tag.id > ctx.fields[FeedFieldLink].tagid) { string_clear(&ctx.fields[FeedFieldLink].str); diff --git a/sfeed_curses.1 b/sfeed_curses.1 @@ -1,4 +1,4 @@ -.Dd February 24, 2022 +.Dd December 20, 2022 .Dt SFEED_CURSES 1 .Os .Sh NAME @@ -98,11 +98,11 @@ height by 1 column. Use a fixed sidebar size for the current layout and increase the fixed width or height by 1 column. .It = -Reset the sidebar size to automaticly adjust for the current layout. +Reset the sidebar size to automatically adjust for the current layout. With the vertical layout the width is the longest feed name with the item counts right-aligned. With the horizontal layout the height is half of the window height (minus the -statusbar) or otherwise the total amount of visible feeds, whichever fits the +status bar) or otherwise the total amount of visible feeds, whichever fits the best. .It t Toggle showing only feeds with new items in the sidebar. @@ -188,13 +188,19 @@ Reload all feed files which were specified as arguments on startup. If .Ev SFEED_URL_FILE is set, it will reload the URLs from this file also. +Cancels the line editor and handles the signal if received during a search. .It SIGINT -Interrupt: when searching it cancels the line editor, otherwise it quits. +Interrupt: quit. +When searching, it only cancels the line editor and doesn't quit. .It SIGTERM Quit .It SIGWINCH Resize the pane dimensions relative to the terminal size. +When searching, it handles the signal after closing the line editor. .El +.Pp +Signals are handled in the following order: SIGCHLD, SIGTERM, SIGINT, SIGHUP, +SIGWINCH. .Sh ENVIRONMENT VARIABLES .Bl -tag -width Ds .It Ev SFEED_AUTOCMD @@ -206,22 +212,13 @@ A program where the whole TAB-Separated Value line is piped to. By default this is "sfeed_content". .It Ev SFEED_PIPER_INTERACTIVE Handle the program interactively in the same terminal or not. -If set to "1" then before execution it restores the terminal attributes and -.Nm -will wait until the program is finished. -If set to "0" then it will suppress stdout and stderr output. By default this is set to "1". .It Ev SFEED_PLUMBER A program that receives the link URL or enclosure URL as a parameter. By default this is "xdg-open". .It Ev SFEED_PLUMBER_INTERACTIVE Handle the program interactively in the same terminal or not. -If set to "1" then before execution it restores the terminal attributes and -.Nm -will wait until the program is finished. -If set to "0" then it will suppress stdout and stderr output. -For example this option is useful to open a text-mode browser in the same -terminal. +This option can be useful to open a text-mode browser in the same terminal. By default this is set to "0". .It Ev SFEED_YANKER A program where the URL or enclosure field is piped to, to copy it to a @@ -229,10 +226,6 @@ clipboard. By default this is "xclip -r". .It Ev SFEED_YANKER_INTERACTIVE Handle the program interactively in the same terminal or not. -If set to "1" then before execution it restores the terminal attributes and -.Nm -will wait until the program is finished. -If set to "0" then it will suppress stdout and stderr output. By default this is set to "0". .It Ev SFEED_URL_FILE If this variable is set then a different mode is used to mark items as read, @@ -267,10 +260,10 @@ It can also cause a race-condition issue if the feed data on disk is changed while having the UI open and offsets for the lines are different. A workaround for the race-condition issue is by sending the SIGHUP signal to .Nm -directly after the data was updated. -This forces +after the data was updated. +This makes .Nm -to reload the latest feed data and update the correct line offsets. +reload the latest feed data and update the correct line offsets. By default this is set to "0". .It Ev SFEED_FEED_PATH This variable is set by @@ -279,8 +272,28 @@ when a feed is loaded. If the data was read from stdin this variable is unset. It can be used by the plumb or pipe program for scripting purposes. .El +.Sh INTERACTIVE AND NON-INTERACTIVE PROGRAMS +.Nm +can pipe content, plumb and yank interactively or in a non-interactive manner. +In interactive mode +.Nm +waits until the process exits. +Stdout and stderr of the program are written as output. +It stores and restores the terminal attributes before and after executing the +program. +The signals SIGHUP and SIGWINCH will be handled after +.Nm +has waited on the program. +SIGINT is ignored while waiting on the program. +.Pp +In non-interactive mode +.Nm +doesn't wait until the process exits. +Stdout and stderr of the program are not written as output. +When plumbing an URL then stdin is closed also. .Sh EXIT STATUS .Ex -std +The exit status is 130 on SIGINT and 143 on SIGTERM. .Sh EXAMPLES .Bd -literal sfeed_curses ~/.sfeed/feeds/* @@ -299,24 +312,26 @@ sfeed_curses ~/.sfeed/feeds/* Which does the following: .Bl -enum .It +Set commands to execute automatically: +.Pp Set the current layout to a horizontal mode ('2' keybind'). Showing a feeds sidebar on the top and the feed items on the bottom. -.It +.Pp Toggle showing only feeds with new items in the sidebar ('t' keybind). -.It +.Pp Go to the first row in the current panel ('g' keybind). -.It +.Pp Load the current selected feed ('o' keybind'). .It Set a file to use for managing read and unread items. -This file is a plain-text file containing a list of read URLs, one URL per -line. +This is a plain-text file containing a list of read URLs, one URL per line. .It Check if this file for managing the read and unread items exists. If it doesn't exist yet then create an empty file. .It Start -.Nm . +.Nm +and read the specified feed files. .El .Sh SEE ALSO .Xr sfeed 1 , diff --git a/sfeed_curses.c b/sfeed_curses.c @@ -4,7 +4,6 @@ #include <sys/types.h> #include <sys/wait.h> -#include <ctype.h> #include <errno.h> #include <fcntl.h> #include <locale.h> @@ -38,10 +37,10 @@ #define LINEBAR_SYMBOL_BAR "\xe2\x94\x80" /* symbol: "light horizontal" */ #define LINEBAR_SYMBOL_RIGHT "\xe2\x94\xa4" /* symbol: "light vertical and left" */ #else -#define SCROLLBAR_SYMBOL_BAR "|" /* symbol: "light vertical" */ +#define SCROLLBAR_SYMBOL_BAR "|" #define SCROLLBAR_SYMBOL_TICK " " -#define LINEBAR_SYMBOL_BAR "-" /* symbol: "light horizontal" */ -#define LINEBAR_SYMBOL_RIGHT "|" /* symbol: "light vertical and left" */ +#define LINEBAR_SYMBOL_BAR "-" +#define LINEBAR_SYMBOL_RIGHT "|" #endif /* color-theme */ @@ -130,10 +129,16 @@ struct item { off_t offset; /* line offset in file for lazyload */ }; +struct urls { + char **items; /* array of URLs */ + size_t len; /* amount of items */ + size_t cap; /* available capacity */ +}; + struct items { - struct item *items; /* array of items */ - size_t len; /* amount of items */ - size_t cap; /* available capacity */ + struct item *items; /* array of items */ + size_t len; /* amount of items */ + size_t cap; /* available capacity */ }; void alldirty(void); @@ -145,9 +150,9 @@ void pane_draw(struct pane *); void sighandler(int); void updategeom(void); void updatesidebar(void); -void urls_free(void); -int urls_isnew(const char *); -void urls_read(void); +void urls_free(struct urls *); +int urls_hasmatch(struct urls *, const char *); +void urls_read(struct urls *, const char *); static struct linebar linebar; static struct statusbar statusbar; @@ -170,10 +175,11 @@ static struct feed *feeds; static struct feed *curfeed; static size_t nfeeds; /* amount of feeds */ static time_t comparetime; -static char *urlfile, **urls; -static size_t nurls; +struct urls urls; +static char *urlfile; -volatile sig_atomic_t sigstate = 0; +volatile sig_atomic_t state_sigchld = 0, state_sighup = 0, state_sigint = 0; +volatile sig_atomic_t state_sigterm = 0, state_sigwinch = 0; static char *plumbercmd = "xdg-open"; /* env variable: $SFEED_PLUMBER */ static char *pipercmd = "sfeed_content"; /* env variable: $SFEED_PIPER */ @@ -208,11 +214,6 @@ ttywrite(const char *s) return write(1, s, strlen(s)); } -/* Hint for compilers and static analyzers that a function exits. */ -#ifndef __dead -#define __dead -#endif - /* Print to stderr, call cleanup() and _exit(). */ __dead void die(const char *fmt, ...) @@ -549,12 +550,13 @@ init(void) cursormode(0); if (usemouse) - mousemode(usemouse); + mousemode(1); memset(&sa, 0, sizeof(sa)); sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; /* require BSD signal semantics */ sa.sa_handler = sighandler; + sigaction(SIGCHLD, &sa, NULL); sigaction(SIGHUP, &sa, NULL); sigaction(SIGINT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); @@ -564,25 +566,28 @@ init(void) void processexit(pid_t pid, int interactive) { - pid_t wpid; struct sigaction sa; - memset(&sa, 0, sizeof(sa)); - sigemptyset(&sa.sa_mask); - sa.sa_flags = SA_RESTART; /* require BSD signal semantics */ - sa.sa_handler = SIG_IGN; - sigaction(SIGINT, &sa, NULL); - if (interactive) { - while ((wpid = wait(NULL)) >= 0 && wpid != pid) - ; + memset(&sa, 0, sizeof(sa)); + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; /* require BSD signal semantics */ + + /* ignore SIGINT (^C) in parent for interactive applications */ + sa.sa_handler = SIG_IGN; + sigaction(SIGINT, &sa, NULL); + + sa.sa_flags = 0; /* SIGTERM: interrupt waitpid(), no SA_RESTART */ + sa.sa_handler = sighandler; + sigaction(SIGTERM, &sa, NULL); + + /* wait for process to change state, ignore errors */ + waitpid(pid, NULL, 0); + init(); updatesidebar(); updategeom(); updatetitle(); - } else { - sa.sa_handler = sighandler; - sigaction(SIGINT, &sa, NULL); } } @@ -606,8 +611,8 @@ pipeitem(const char *cmd, struct item *item, int field, int interactive) die("fork"); case 0: if (!interactive) { - dup2(devnullfd, 1); - dup2(devnullfd, 2); + dup2(devnullfd, 1); /* stdout */ + dup2(devnullfd, 2); /* stderr */ } errno = 0; @@ -644,8 +649,9 @@ forkexec(char *argv[], int interactive) die("fork"); case 0: if (!interactive) { - dup2(devnullfd, 1); - dup2(devnullfd, 2); + dup2(devnullfd, 0); /* stdin */ + dup2(devnullfd, 1); /* stdout */ + dup2(devnullfd, 2); /* stderr */ } if (execvp(argv[0], argv) == -1) _exit(1); @@ -985,6 +991,8 @@ lineeditor(void) size_t cap = 0, nchars = 0; int ch; + if (usemouse) + mousemode(0); for (;;) { if (nchars + 2 >= cap) { cap = cap ? cap * 2 : 32; @@ -1006,23 +1014,26 @@ lineeditor(void) ttywrite(&input[nchars]); nchars++; } else if (ch < 0) { - switch (sigstate) { - case 0: - case SIGWINCH: - /* continue editing: process signal later */ - continue; - case SIGINT: - /* cancel prompt, but do not quit */ - sigstate = 0; /* reset: do not handle it */ - break; - default: /* other: SIGHUP, SIGTERM */ - /* cancel prompt and handle signal after */ - break; + if (state_sigchld) { + state_sigchld = 0; + /* wait on child processes so they don't become a zombie */ + while (waitpid((pid_t)-1, NULL, WNOHANG) > 0) + ; } + if (state_sigint) + state_sigint = 0; /* cancel prompt and don't handle this signal */ + else if (state_sighup || state_sigterm) + ; /* cancel prompt and handle these signals */ + else /* no signal, time-out or SIGCHLD or SIGWINCH */ + continue; /* do not cancel: process signal later */ + free(input); - return NULL; + input = NULL; + break; /* cancel prompt */ } } + if (usemouse) + mousemode(1); return input; } @@ -1192,9 +1203,9 @@ feed_items_get(struct feed *f, FILE *fp, struct items *itemsret) if (n <= 0 || feof(fp)) break; } - itemsret->cap = cap; itemsret->items = items; itemsret->len = nitems; + itemsret->cap = cap; free(line); } @@ -1213,7 +1224,7 @@ updatenewitems(struct feed *f) row = &(p->rows[i]); /* do not use pane_row_get() */ item = row->data; if (urlfile) - item->isnew = urls_isnew(item->matchnew); + item->isnew = !urls_hasmatch(&urls, item->matchnew); else item->isnew = (item->timeok && item->timestamp >= comparetime); row->bold = item->isnew; @@ -1259,7 +1270,7 @@ feed_count(struct feed *f, FILE *fp) parseline(line, fields); if (urlfile) { - f->totalnew += urls_isnew(fields[fields[FieldLink][0] ? FieldLink : FieldId]); + f->totalnew += !urls_hasmatch(&urls, fields[fields[FieldLink][0] ? FieldLink : FieldId]); } else { parsedtime = 0; if (!strtotime(fields[FieldUnixTimestamp], &parsedtime)) @@ -1378,9 +1389,9 @@ feeds_reloadall(void) pos = panes[PaneItems].pos; /* store numeric item position */ feeds_set(curfeed); /* close and reopen feed if possible */ - urls_read(); + urls_read(&urls, urlfile); feeds_load(feeds, nfeeds); - urls_free(); + urls_free(&urls); /* restore numeric item position */ pane_setpos(&panes[PaneItems], pos); updatesidebar(); @@ -1403,10 +1414,10 @@ feed_open_selected(struct pane *p) return; f = row->data; feeds_set(f); - urls_read(); + urls_read(&urls, urlfile); if (f->fp) feed_load(f, f->fp); - urls_free(); + urls_free(&urls); /* redraw row: counts could be changed */ updatesidebar(); updatetitle(); @@ -1422,13 +1433,15 @@ feed_plumb_selected_item(struct pane *p, int field) { struct row *row; struct item *item; - char *cmd[] = { plumbercmd, NULL, NULL }; + char *cmd[3]; /* will have: { plumbercmd, arg, NULL } */ if (!(row = pane_row_get(p, p->pos))) return; markread(p, p->pos, p->pos, 1); item = row->data; + cmd[0] = plumbercmd; cmd[1] = item->fields[field]; /* set first argument for plumber */ + cmd[2] = NULL; forkexec(cmd, plumberia); } @@ -1580,14 +1593,11 @@ void sighandler(int signo) { switch (signo) { - case SIGHUP: - case SIGINT: - case SIGTERM: - case SIGWINCH: - /* SIGTERM is more important, do not override it */ - if (sigstate != SIGTERM) - sigstate = signo; - break; + case SIGCHLD: state_sigchld = 1; break; + case SIGHUP: state_sighup = 1; break; + case SIGINT: state_sigint = 1; break; + case SIGTERM: state_sigterm = 1; break; + case SIGWINCH: state_sigwinch = 1; break; } } @@ -1836,7 +1846,7 @@ markread(struct pane *p, off_t from, off_t to, int isread) FILE *fp; off_t i; const char *cmd; - int isnew = !isread, pid, wpid, status, visstart; + int isnew = !isread, pid, status = -1, visstart; if (!urlfile || !p->nrows) return; @@ -1847,8 +1857,8 @@ markread(struct pane *p, off_t from, off_t to, int isread) case -1: die("fork"); case 0: - dup2(devnullfd, 1); - dup2(devnullfd, 2); + dup2(devnullfd, 1); /* stdout */ + dup2(devnullfd, 2); /* stderr */ errno = 0; if (!(fp = popen(cmd, "w"))) @@ -1867,11 +1877,9 @@ markread(struct pane *p, off_t from, off_t to, int isread) status = WIFEXITED(status) ? WEXITSTATUS(status) : 127; _exit(status); default: - while ((wpid = wait(&status)) >= 0 && wpid != pid) - ; - - /* fail: exit statuscode was non-zero */ - if (status) + /* waitpid() and block on process status change, + fail if exit statuscode was unavailable or non-zero */ + if (waitpid(pid, &status, 0) <= 0 || status) break; visstart = p->pos - (p->pos % p->height); /* visible start */ @@ -1899,32 +1907,35 @@ urls_cmp(const void *v1, const void *v2) return strcmp(*((char **)v1), *((char **)v2)); } -int -urls_isnew(const char *url) +void +urls_free(struct urls *urls) { - return (!nurls || - bsearch(&url, urls, nurls, sizeof(char *), urls_cmp) == NULL); + while (urls->len > 0) { + urls->len--; + free(urls->items[urls->len]); + } + free(urls->items); + urls->items = NULL; + urls->len = 0; + urls->cap = 0; } -void -urls_free(void) +int +urls_hasmatch(struct urls *urls, const char *url) { - while (nurls > 0) - free(urls[--nurls]); - free(urls); - urls = NULL; - nurls = 0; + return (urls->len && + bsearch(&url, urls->items, urls->len, sizeof(char *), urls_cmp)); } void -urls_read(void) +urls_read(struct urls *urls, const char *urlfile) { FILE *fp; char *line = NULL; - size_t linesiz = 0, cap = 0; + size_t linesiz = 0; ssize_t n; - urls_free(); + urls_free(urls); if (!urlfile) return; @@ -1934,19 +1945,19 @@ urls_read(void) while ((n = getline(&line, &linesiz, fp)) > 0) { if (line[n - 1] == '\n') line[--n] = '\0'; - if (nurls + 1 >= cap) { - cap = cap ? cap * 2 : 16; - urls = erealloc(urls, cap * sizeof(char *)); + if (urls->len + 1 >= urls->cap) { + urls->cap = urls->cap ? urls->cap * 2 : 16; + urls->items = erealloc(urls->items, urls->cap * sizeof(char *)); } - urls[nurls++] = estrdup(line); + urls->items[urls->len++] = estrdup(line); } if (ferror(fp)) die("getline: %s", urlfile); fclose(fp); free(line); - if (nurls > 0) - qsort(urls, nurls, sizeof(char *), urls_cmp); + if (urls->len > 0) + qsort(urls->items, urls->len, sizeof(char *), urls_cmp); } int @@ -2014,9 +2025,9 @@ main(int argc, char *argv[]) nfeeds = argc - 1; } feeds_set(&feeds[0]); - urls_read(); + urls_read(&urls, urlfile); feeds_load(feeds, nfeeds); - urls_free(); + urls_free(&urls); if (!isatty(0)) { if ((fd = open("/dev/tty", O_RDONLY)) == -1) @@ -2108,12 +2119,15 @@ main(int argc, char *argv[]) mousereport(button, release, keymask, x - 1, y - 1); break; + /* DEC/SUN: ESC O char, HP: ESC char or SCO: ESC [ char */ case 'A': goto keyup; /* arrow up */ case 'B': goto keydown; /* arrow down */ case 'C': goto keyright; /* arrow right */ case 'D': goto keyleft; /* arrow left */ case 'F': goto endpos; /* end */ + case 'G': goto nextpage; /* page down */ case 'H': goto startpos; /* home */ + case 'I': goto prevpage; /* page up */ default: if (!(ch >= '0' && ch <= '9')) break; @@ -2132,6 +2146,13 @@ main(int argc, char *argv[]) case 7: goto startpos; /* home: urxvt */ case 8: goto endpos; /* end: urxvt */ } + } else if (ch == 'z') { /* SUN: ESC [ num z */ + switch (i) { + case 214: goto startpos; /* home */ + case 216: goto prevpage; /* page up */ + case 220: goto endpos; /* end */ + case 222: goto nextpage; /* page down */ + } } break; } @@ -2337,23 +2358,35 @@ nextpage: event: if (ch == EOF) goto end; - else if (ch == -3 && sigstate == 0) + else if (ch == -3 && !state_sigchld && !state_sighup && + !state_sigint && !state_sigterm && !state_sigwinch) continue; /* just a time-out, nothing to do */ - switch (sigstate) { - case SIGHUP: - feeds_reloadall(); - sigstate = 0; - break; - case SIGINT: - case SIGTERM: + /* handle signals in a particular order */ + if (state_sigchld) { + state_sigchld = 0; + /* wait on child processes so they don't become a zombie, + do not block the parent process if there is no status, + ignore errors */ + while (waitpid((pid_t)-1, NULL, WNOHANG) > 0) + ; + } + if (state_sigterm) { cleanup(); - _exit(128 + sigstate); - case SIGWINCH: + _exit(128 + SIGTERM); + } + if (state_sigint) { + cleanup(); + _exit(128 + SIGINT); + } + if (state_sighup) { + state_sighup = 0; + feeds_reloadall(); + } + if (state_sigwinch) { + state_sigwinch = 0; resizewin(); updategeom(); - sigstate = 0; - break; } draw(); diff --git a/sfeed_gopher.1 b/sfeed_gopher.1 @@ -1,4 +1,4 @@ -.Dd July 31, 2021 +.Dd May 14, 2022 .Dt SFEED_GOPHER 1 .Os .Sh NAME @@ -32,7 +32,7 @@ written to stdout and no files are written. .Pp Items with a timestamp from the last day compared to the system time at the time of formatting are counted and marked as new. -Items are marked as new with the prefix "N". +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 @@ -50,7 +50,7 @@ The default is "70". .Ex -std .Sh EXAMPLES .Bd -literal -SFEED_GOPHER_HOST="codemadness.org" SFEED_GOPHER_PATH="/feeds/" \\ +SFEED_GOPHER_HOST="codemadness.org" SFEED_GOPHER_PATH="/feeds/" \e sfeed_gopher ~/.sfeed/feeds/* .Ed .Sh SEE ALSO diff --git a/sfeed_gopher.c b/sfeed_gopher.c @@ -1,6 +1,5 @@ #include <sys/types.h> -#include <limits.h> #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -65,7 +64,7 @@ printfeed(FILE *fpitems, FILE *fpin, struct feed *f) if (fields[FieldLink][0]) { itemtype = 'h'; - /* if it's a gopher URL then change it into a direntry */ + /* if it's a gopher URL then change it into a DirEntity */ if (!strncmp(fields[FieldLink], "gopher://", 9) && uri_parse(fields[FieldLink], &u) != -1) { itemhost = u.host; @@ -123,8 +122,8 @@ int main(int argc, char *argv[]) { FILE *fpitems, *fpindex, *fp; - char *name, *p, path[PATH_MAX + 1]; - int i, r; + char *name, *p; + int i; if (argc == 1) { if (pledge("stdio", NULL) == -1) @@ -168,15 +167,11 @@ main(int argc, char *argv[]) if (!(fp = fopen(argv[i], "r"))) err(1, "fopen: %s", argv[i]); - - r = snprintf(path, sizeof(path), "%s", name); - if (r < 0 || (size_t)r >= sizeof(path)) - errx(1, "path truncation: %s", path); - if (!(fpitems = fopen(path, "wb"))) + if (!(fpitems = fopen(name, "wb"))) err(1, "fopen"); printfeed(fpitems, fp, &f); checkfileerror(fp, argv[i], 'r'); - checkfileerror(fpitems, path, 'w'); + checkfileerror(fpitems, name, 'w'); fclose(fp); fclose(fpitems); @@ -185,7 +180,7 @@ main(int argc, char *argv[]) gophertext(fpindex, name); fprintf(fpindex, " (%lu/%lu)\t", f.totalnew, f.total); gophertext(fpindex, prefixpath); - gophertext(fpindex, path); + gophertext(fpindex, name); fprintf(fpindex, "\t%s\t%s\r\n", host, port); } fputs(".\r\n", fpindex); diff --git a/sfeed_markread b/sfeed_markread @@ -2,14 +2,14 @@ # Mark items as read/unread: the input is the read / unread URL per line. usage() { - echo "usage: $0 <read|unread> [urlfile]" >&2 + printf "usage: %s <read|unread> [urlfile]\n" "$0" >&2 echo "" >&2 echo "An urlfile must be specified as an argument or with the environment variable \$SFEED_URL_FILE" >&2 exit 1 } urlfile="${2:-${SFEED_URL_FILE}}" -if test -z "${urlfile}"; then +if [ -z "${urlfile}" ]; then usage fi @@ -20,7 +20,7 @@ read) unread) tmp=$(mktemp) trap "rm -f ${tmp}" EXIT - test -f "${urlfile}" || touch "${urlfile}" 2>/dev/null + [ -f "${urlfile}" ] || touch "${urlfile}" 2>/dev/null LC_ALL=C awk -F '\t' ' { FILENR += (FNR == 1) } FILENR == 1 { urls[$0] = 1 } diff --git a/sfeed_mbox.c b/sfeed_mbox.c @@ -22,8 +22,8 @@ djb2(unsigned char *s, unsigned long long hash) } /* Unescape / decode fields printed by string_print_encoded() - * "\\" to "\", "\t", to TAB, "\n" to newline. Unrecognised escape sequences - * are ignored: "\z" etc. Mangle "From " in mboxrd style (always prefix >). */ + * "\\" to "\", "\t", to TAB, "\n" to newline. Other escape sequences are + * ignored: "\z" etc. Mangle "From " in mboxrd style (always prefix >). */ static void printcontent(const char *s, FILE *fp) { diff --git a/sfeed_opml_export b/sfeed_opml_export @@ -18,8 +18,8 @@ loadconfig() { if [ -r "${path}" ]; then . "${path}" else - echo "Configuration file \"${config}\" cannot be read." >&2 - echo "See sfeedrc.example for an example." >&2 + printf "Configuration file \"%s\" cannot be read.\n" "${config}" >&2 + echo "See the sfeedrc.example file or the sfeedrc(5) man page for an example." >&2 exit 1 fi } @@ -27,8 +27,8 @@ loadconfig() { # override feed function to output OPML XML. # feed(name, feedurl, [basesiteurl], [encoding]) feed() { - # TABs, newlines and echo options in field values are not checked. - echo "$1 $2" + # uses the characters 0x1f and 0x1e as a separator. + printf '%s\037%s\036' "$1" "$2" } # load config file. @@ -43,7 +43,11 @@ cat <<! <body> ! -feeds | awk -F '\t' '{ +feeds | LC_ALL=C awk ' +BEGIN { + FS = "\x1f"; RS = "\x1e"; +} +{ gsub("&", "\\&amp;"); gsub("\"", "\\&quot;"); gsub("'"'"'", "\\&#39;"); diff --git a/sfeed_opml_import.c b/sfeed_opml_import.c @@ -1,4 +1,3 @@ -#include <ctype.h> #include <stdio.h> #include <strings.h> @@ -12,7 +11,7 @@ static void printsafe(const char *s) { for (; *s; s++) { - if (iscntrl((unsigned char)*s)) + if (ISCNTRL((unsigned char)*s)) continue; else if (*s == '\\') fputs("\\\\", stdout); diff --git a/sfeed_plain.1 b/sfeed_plain.1 @@ -1,4 +1,4 @@ -.Dd July 25, 2021 +.Dd May 14, 2022 .Dt SFEED_PLAIN 1 .Os .Sh NAME @@ -26,7 +26,7 @@ is empty. .Pp Items with a timestamp from the last day compared to the system time at the time of formatting are marked as new. -Items are marked as new with the prefix "N". +Items are marked as new with the prefix "N" at the start of the line. .Pp .Nm aligns the output. diff --git a/sfeed_update b/sfeed_update @@ -27,8 +27,8 @@ loadconfig() { if [ -r "${path}" ]; then . "${path}" else - echo "Configuration file \"${config}\" cannot be read." >&2 - echo "See sfeedrc.example for an example." >&2 + printf "Configuration file \"%s\" cannot be read.\n" "${config}" >&2 + echo "See the sfeedrc.example file or the sfeedrc(5) man page for an example." >&2 exit 1 fi } @@ -182,12 +182,12 @@ sighandler() { signo="$1" # ignore TERM signal for myself. trap -- "" TERM - # kill all running childs >:D + # kill all running children >:D kill -TERM -$$ } feeds() { - echo "Configuration file \"${config}\" is invalid or does not contain a \"feeds\" function." >&2 + printf "Configuration file \"%s\" is invalid or does not contain a \"feeds\" function.\n" "${config}" >&2 echo "See sfeedrc.example for an example." >&2 } @@ -213,13 +213,13 @@ main() { # wait till all feeds are fetched (concurrently). [ ${signo} -eq 0 ] && wait # check error exit status indicator for parallel jobs. - test -f "${sfeedtmpdir}/ok" + [ -f "${sfeedtmpdir}/ok" ] status=$? # cleanup temporary files etc. cleanup # on signal SIGINT and SIGTERM exit with signal number + 128. [ ${signo} -ne 0 ] && exit $((signo+128)) - return ${status} + exit ${status} } [ "${SFEED_UPDATE_INCLUDE}" = "1" ] || main "$@" diff --git a/sfeed_update.1 b/sfeed_update.1 @@ -1,4 +1,4 @@ -.Dd March 21, 2022 +.Dd December 15, 2022 .Dt SFEED_UPDATE 1 .Os .Sh NAME @@ -62,16 +62,17 @@ stdout in the format: .Ed .Sh EXIT STATUS .Ex -std +If any of the feeds failed to update then the exit status is non-zero. .Sh EXAMPLES To update your feeds and format them in various formats: .Bd -literal -# Update +# Update feeds sfeed_update "configfile" -# Plain-text list +# Format to a plain-text list sfeed_plain ~/.sfeed/feeds/* > ~/.sfeed/feeds.txt -# HTML +# Format to HTML sfeed_html ~/.sfeed/feeds/* > ~/.sfeed/feeds.html -# HTML with frames +# Format to HTML with frames mkdir -p somedir && cd somedir && sfeed_frames ~/.sfeed/feeds/* .Ed .Sh SEE ALSO diff --git a/sfeed_web.c b/sfeed_web.c @@ -1,4 +1,3 @@ -#include <ctype.h> #include <stdio.h> #include <strings.h> @@ -16,7 +15,7 @@ static void printvalue(const char *s) { for (; *s; s++) - if (!iscntrl((unsigned char)*s)) + if (!ISCNTRL((unsigned char)*s)) putchar(*s); } diff --git a/sfeed_xmlenc.c b/sfeed_xmlenc.c @@ -1,4 +1,3 @@ -#include <ctype.h> #include <stdio.h> #include <stdlib.h> #include <strings.h> @@ -26,10 +25,10 @@ xmlattr(XMLParser *p, const char *t, size_t tl, const char *n, size_t nl, return; for (; *v; v++) { - if (isalpha((unsigned char)*v) || - isdigit((unsigned char)*v) || + if (ISALPHA((unsigned char)*v) || + ISDIGIT((unsigned char)*v) || *v == '.' || *v == ':' || *v == '-' || *v == '_') - putchar(tolower((unsigned char)*v)); + putchar(TOLOWER((unsigned char)*v)); } } diff --git a/sfeedrc.5 b/sfeedrc.5 @@ -1,4 +1,4 @@ -.Dd March 7, 2022 +.Dd January 18, 2023 .Dt SFEEDRC 5 .Os .Sh NAME @@ -59,12 +59,12 @@ is a shellscript each function can be overridden to change its behaviour, notable functions are: .Bl -tag -width Ds .It Fn fetch "name" "url" "feedfile" -Fetch feed from URL and writes data to stdout, its arguments are: +Fetch feed from URL and write the data to stdout, its arguments are: .Bl -tag -width Ds .It Fa name Specified name in configuration file (useful for logging). .It Fa url -Url to fetch. +URL to fetch. .It Fa feedfile Used feedfile (useful for comparing modification times). .El @@ -73,8 +73,9 @@ By default the tool .Xr curl 1 is used. .It Fn convertencoding "name" "from" "to" -Convert from text-encoding to another and writes it to stdout, its arguments -are: +Convert data from stdin from one text-encoding to another and write it to +stdout, +its arguments are: .Bl -tag -width Ds .It Fa name Feed name. @@ -88,9 +89,9 @@ By default the tool .Xr iconv 1 is used. .It Fn parse "name" "feedurl" "basesiteurl" -Parse and convert RSS/Atom XML to the +Read RSS/Atom XML data from stdin, convert and write it as .Xr sfeed 5 -TSV format. +data to stdout. .Bl -tag -width Ds .It Fa name Name of the feed. @@ -103,13 +104,15 @@ This argument allows to fix relative item links. .It Fn filter "name" Filter .Xr sfeed 5 -data from stdin, write to stdout, its arguments are: +data from stdin and write it to stdout, its arguments are: .Bl -tag -width Ds .It Fa name Feed name. .El .It Fn merge "name" "oldfile" "newfile" -Merge data of oldfile with newfile and writes it to stdout, its arguments are: +Merge +.Xr sfeed 5 +data of oldfile with newfile and write it to stdout, its arguments are: .Bl -tag -width Ds .It Fa name Feed name. @@ -121,7 +124,7 @@ New file. .It Fn order "name" Sort .Xr sfeed 5 -data from stdin, write to stdout, its arguments are: +data from stdin and write it to stdout, its arguments are: .Bl -tag -width Ds .It Fa name Feed name. @@ -160,7 +163,7 @@ file: # fetch(name, url, feedfile) fetch() { # allow for 1 redirect, hide User-Agent, timeout is 15 seconds. - curl -L --max-redirs 1 -H "User-Agent:" -f -s -m 15 \\ + curl -L --max-redirs 1 -H "User-Agent:" -f -s -m 15 \e "$2" 2>/dev/null } .Ed diff --git a/themes/mono.h b/themes/mono.h @@ -1,13 +1,13 @@ /* default mono theme */ -#define THEME_ITEM_NORMAL() do { } while(0) -#define THEME_ITEM_FOCUS() do { } while(0) +#define THEME_ITEM_NORMAL() +#define THEME_ITEM_FOCUS() #define THEME_ITEM_BOLD() do { attrmode(ATTR_BOLD_ON); } while(0) #define THEME_ITEM_SELECTED() do { if (p->focused) attrmode(ATTR_REVERSE_ON); } while(0) -#define THEME_SCROLLBAR_FOCUS() do { } while(0) +#define THEME_SCROLLBAR_FOCUS() #define THEME_SCROLLBAR_NORMAL() do { attrmode(ATTR_FAINT_ON); } while(0) #define THEME_SCROLLBAR_TICK_FOCUS() do { attrmode(ATTR_REVERSE_ON); } while(0) #define THEME_SCROLLBAR_TICK_NORMAL() do { attrmode(ATTR_REVERSE_ON); } while(0) #define THEME_LINEBAR() do { attrmode(ATTR_FAINT_ON); } while(0) #define THEME_STATUSBAR() do { attrmode(ATTR_REVERSE_ON); } while(0) #define THEME_INPUT_LABEL() do { attrmode(ATTR_REVERSE_ON); } while(0) -#define THEME_INPUT_NORMAL() do { } while(0) +#define THEME_INPUT_NORMAL() diff --git a/themes/mono_highlight.h b/themes/mono_highlight.h @@ -1,15 +1,15 @@ /* mono theme with highlighting of the active panel. The faint attribute may not work on all terminals though. The combination bold with faint generally does not work either. */ -#define THEME_ITEM_NORMAL() do { } while(0) -#define THEME_ITEM_FOCUS() do { } while(0) +#define THEME_ITEM_NORMAL() +#define THEME_ITEM_FOCUS() #define THEME_ITEM_BOLD() do { if (p->focused || !selected) attrmode(ATTR_BOLD_ON); } while(0) #define THEME_ITEM_SELECTED() do { attrmode(ATTR_REVERSE_ON); if (!p->focused) attrmode(ATTR_FAINT_ON); } while(0) -#define THEME_SCROLLBAR_FOCUS() do { } while(0) +#define THEME_SCROLLBAR_FOCUS() #define THEME_SCROLLBAR_NORMAL() do { attrmode(ATTR_FAINT_ON); } while(0) #define THEME_SCROLLBAR_TICK_FOCUS() do { attrmode(ATTR_REVERSE_ON); } while(0) #define THEME_SCROLLBAR_TICK_NORMAL() do { attrmode(ATTR_REVERSE_ON); } while(0) #define THEME_LINEBAR() do { attrmode(ATTR_FAINT_ON); } while(0) #define THEME_STATUSBAR() do { attrmode(ATTR_REVERSE_ON); } while(0) #define THEME_INPUT_LABEL() do { attrmode(ATTR_REVERSE_ON); } while(0) -#define THEME_INPUT_NORMAL() do { } while(0) +#define THEME_INPUT_NORMAL() diff --git a/themes/newsboat.h b/themes/newsboat.h @@ -1,6 +1,6 @@ /* newsboat-like (blue, yellow) */ -#define THEME_ITEM_NORMAL() do { } while(0) -#define THEME_ITEM_FOCUS() do { } while(0) +#define THEME_ITEM_NORMAL() +#define THEME_ITEM_FOCUS() #define THEME_ITEM_BOLD() do { attrmode(ATTR_BOLD_ON); } while(0) #define THEME_ITEM_SELECTED() do { if (p->focused) ttywrite("\x1b[93;44m"); } while(0) /* bright yellow fg, blue bg */ #define THEME_SCROLLBAR_FOCUS() do { ttywrite("\x1b[34m"); } while(0) /* blue fg */ @@ -9,5 +9,5 @@ #define THEME_SCROLLBAR_TICK_NORMAL() do { ttywrite("\x1b[44m"); } while(0) #define THEME_LINEBAR() do { ttywrite("\x1b[34m"); } while(0) #define THEME_STATUSBAR() do { attrmode(ATTR_BOLD_ON); ttywrite("\x1b[93;44m"); } while(0) -#define THEME_INPUT_LABEL() do { } while(0) -#define THEME_INPUT_NORMAL() do { } while(0) +#define THEME_INPUT_LABEL() +#define THEME_INPUT_NORMAL() diff --git a/util.c b/util.c @@ -1,4 +1,3 @@ -#include <ctype.h> #include <errno.h> #include <stdarg.h> #include <stdio.h> @@ -66,8 +65,8 @@ strcasestr(const char *h, const char *n) return (char *)h; for (; *h; ++h) { - for (i = 0; n[i] && tolower((unsigned char)n[i]) == - tolower((unsigned char)h[i]); ++i) + for (i = 0; n[i] && TOLOWER((unsigned char)n[i]) == + TOLOWER((unsigned char)h[i]); ++i) ; if (n[i] == '\0') return (char *)h; @@ -82,7 +81,7 @@ uri_hasscheme(const char *s) { const char *p = s; - for (; isalpha((unsigned char)*p) || isdigit((unsigned char)*p) || + for (; ISALPHA((unsigned char)*p) || ISDIGIT((unsigned char)*p) || *p == '+' || *p == '-' || *p == '.'; p++) ; /* scheme, except if empty and starts with ":" then it is a path */ @@ -109,7 +108,7 @@ uri_parse(const char *s, struct uri *u) } /* scheme / protocol part */ - for (; isalpha((unsigned char)*p) || isdigit((unsigned char)*p) || + for (; ISALPHA((unsigned char)*p) || ISDIGIT((unsigned char)*p) || *p == '+' || *p == '-' || *p == '.'; p++) ; /* scheme, except if empty and starts with ":" then it is a path */ diff --git a/util.h b/util.h @@ -8,6 +8,13 @@ #define unveil(p1,p2) 0 #endif +/* ctype-like macros, but always compatible with ASCII / UTF-8 */ +#define ISALPHA(c) ((((unsigned)c) | 32) - 'a' < 26) +#define ISCNTRL(c) ((c) < ' ' || (c) == 0x7f) +#define ISDIGIT(c) (((unsigned)c) - '0' < 10) +#define ISSPACE(c) ((c) == ' ' || ((((unsigned)c) - '\t') < 5)) +#define TOLOWER(c) ((((unsigned)c) - 'A' < 26) ? ((c) | 32) : (c)) + #undef strcasestr char *strcasestr(const char *, const char *); #undef strlcat @@ -19,8 +26,8 @@ size_t strlcpy(char *, const char *, size_t); #define PAD_TRUNCATE_SYMBOL "\xe2\x80\xa6" /* symbol: "ellipsis" */ #define UTF_INVALID_SYMBOL "\xef\xbf\xbd" /* symbol: "replacement" */ #else -#define PAD_TRUNCATE_SYMBOL "." /* symbol: "ellipsis" */ -#define UTF_INVALID_SYMBOL "?" /* symbol: "replacement" */ +#define PAD_TRUNCATE_SYMBOL "." +#define UTF_INVALID_SYMBOL "?" #endif /* feed info */ diff --git a/xml.c b/xml.c @@ -1,4 +1,3 @@ -#include <ctype.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> @@ -6,6 +5,9 @@ #include "xml.h" +#define ISALPHA(c) ((((unsigned)c) | 32) - 'a' < 26) +#define ISSPACE(c) ((c) == ' ' || ((((unsigned)c) - '\t') < 5)) + static void xml_parseattrs(XMLParser *x) { @@ -13,7 +15,7 @@ xml_parseattrs(XMLParser *x) int c, endsep, endname = 0, valuestart = 0; while ((c = GETNEXT()) != EOF) { - if (isspace(c)) { + if (ISSPACE(c)) { if (namelen) endname = 1; continue; @@ -23,7 +25,7 @@ xml_parseattrs(XMLParser *x) x->name[namelen] = '\0'; valuestart = 1; endname = 1; - } else if (namelen && ((endname && !valuestart && isalpha(c)) || (c == '>' || c == '/'))) { + } else if (namelen && ((endname && !valuestart && ISALPHA(c)) || (c == '>' || c == '/'))) { /* attribute without value */ x->name[namelen] = '\0'; if (x->xmlattrstart) @@ -44,7 +46,7 @@ xml_parseattrs(XMLParser *x) if (c == '\'' || c == '"') { endsep = c; } else { - endsep = ' '; /* isspace() */ + endsep = ' '; /* ISSPACE() */ goto startvalue; } @@ -58,7 +60,7 @@ startvalue: x->data[0] = c; valuelen = 1; while ((c = GETNEXT()) != EOF) { - if (c == endsep || (endsep == ' ' && (c == '>' || isspace(c)))) + if (c == endsep || (endsep == ' ' && (c == '>' || ISSPACE(c)))) break; if (valuelen < sizeof(x->data) - 1) x->data[valuelen++] = c; @@ -79,7 +81,7 @@ startvalue: break; } } - } else if (c != endsep && !(endsep == ' ' && (c == '>' || isspace(c)))) { + } else if (c != endsep && !(endsep == ' ' && (c == '>' || ISSPACE(c)))) { if (valuelen < sizeof(x->data) - 1) { x->data[valuelen++] = c; } else { @@ -90,7 +92,7 @@ startvalue: valuelen = 1; } } - if (c == endsep || (endsep == ' ' && (c == '>' || isspace(c)))) { + if (c == endsep || (endsep == ' ' && (c == '>' || ISSPACE(c)))) { x->data[valuelen] = '\0'; if (x->xmlattr) x->xmlattr(x, x->tag, x->taglen, x->name, namelen, x->data, valuelen); @@ -290,7 +292,7 @@ xml_parse(XMLParser *x) if ((c = GETNEXT()) == EOF) return; - if (c == '!') { /* cdata and comments */ + if (c == '!') { /* CDATA and comments */ for (tagdatalen = 0; (c = GETNEXT()) != EOF;) { /* NOTE: sizeof(x->data) must be at least sizeof("[CDATA[") */ if (tagdatalen <= sizeof("[CDATA[") - 1) @@ -328,7 +330,7 @@ xml_parse(XMLParser *x) while ((c = GETNEXT()) != EOF) { if (c == '/') x->isshorttag = 1; /* short tag */ - else if (c == '>' || isspace(c)) { + else if (c == '>' || ISSPACE(c)) { x->tag[x->taglen] = '\0'; if (isend) { /* end tag, starts with </ */ if (x->xmltagend) @@ -339,7 +341,7 @@ xml_parse(XMLParser *x) /* start tag */ if (x->xmltagstart) x->xmltagstart(x, x->tag, x->taglen); - if (isspace(c)) + if (ISSPACE(c)) xml_parseattrs(x); if (x->xmltagstartparsed) x->xmltagstartparsed(x, x->tag, x->taglen, x->isshorttag); diff --git a/xml.h b/xml.h @@ -30,11 +30,11 @@ typedef struct xmlparser { /* current tag */ char tag[1024]; size_t taglen; - /* current tag is in short form ? <tag /> */ + /* current tag is in shortform ? <tag /> */ int isshorttag; /* current attribute name */ char name[1024]; - /* data buffer used for tag data, cdata and attribute data */ + /* data buffer used for tag data, CDATA and attribute data */ char data[BUFSIZ]; } XMLParser;