diff options
-rw-r--r-- | LICENSE | 2 | ||||
-rw-r--r-- | Makefile | 9 | ||||
-rw-r--r-- | README | 335 | ||||
-rw-r--r-- | README.xml | 4 | ||||
-rw-r--r-- | minicurses.h | 2 | ||||
-rw-r--r-- | sfeed.1 | 20 | ||||
-rw-r--r-- | sfeed.5 | 9 | ||||
-rw-r--r-- | sfeed.c | 160 | ||||
-rw-r--r-- | sfeed_atom.c | 25 | ||||
-rw-r--r-- | sfeed_curses.1 | 87 | ||||
-rw-r--r-- | sfeed_curses.c | 490 | ||||
-rw-r--r-- | sfeed_frames.1 | 7 | ||||
-rw-r--r-- | sfeed_frames.c | 23 | ||||
-rw-r--r-- | sfeed_gopher.1 | 10 | ||||
-rw-r--r-- | sfeed_gopher.c | 37 | ||||
-rw-r--r-- | sfeed_html.1 | 7 | ||||
-rw-r--r-- | sfeed_html.c | 20 | ||||
-rw-r--r-- | sfeed_json.1 | 49 | ||||
-rw-r--r-- | sfeed_json.c | 172 | ||||
-rwxr-xr-x | sfeed_markread | 8 | ||||
-rw-r--r-- | sfeed_mbox.c | 34 | ||||
-rwxr-xr-x | sfeed_opml_export | 24 | ||||
-rw-r--r-- | sfeed_opml_import.c | 10 | ||||
-rw-r--r-- | sfeed_plain.1 | 11 | ||||
-rw-r--r-- | sfeed_plain.c | 17 | ||||
-rw-r--r-- | sfeed_twtxt.c | 12 | ||||
-rwxr-xr-x | sfeed_update | 139 | ||||
-rw-r--r-- | sfeed_update.1 | 19 | ||||
-rw-r--r-- | sfeed_web.c | 10 | ||||
-rw-r--r-- | sfeed_xmlenc.c | 12 | ||||
-rw-r--r-- | sfeedrc.5 | 42 | ||||
-rw-r--r-- | sfeedrc.example | 5 | ||||
-rw-r--r-- | style.css | 9 | ||||
-rw-r--r-- | themes/mono.h | 8 | ||||
-rw-r--r-- | themes/mono_highlight.h | 8 | ||||
-rw-r--r-- | themes/newsboat.h | 8 | ||||
-rw-r--r-- | util.c | 37 | ||||
-rw-r--r-- | util.h | 16 | ||||
-rw-r--r-- | xml.c | 28 | ||||
-rw-r--r-- | xml.h | 8 |
40 files changed, 1277 insertions, 656 deletions
@@ -1,6 +1,6 @@ ISC License -Copyright (c) 2011-2022 Hiltjo Posthuma <hiltjo@codemadness.org> +Copyright (c) 2011-2024 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 @@ -1,7 +1,7 @@ .POSIX: NAME = sfeed -VERSION = 1.2 +VERSION = 2.0 # curses theme, see themes/ directory. SFEED_THEME = mono @@ -26,7 +26,7 @@ SFEED_CPPFLAGS = -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=700 -D_BSD_SOURCE SFEED_CURSES = sfeed_curses SFEED_CURSES_CFLAGS = ${CFLAGS} SFEED_CURSES_CPPFLAGS = -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=700 -D_BSD_SOURCE \ - -DSFEED_THEME=\"themes/${SFEED_THEME}.h\" ${SFEED_CPPFLAGS} + -DSFEED_THEME=\"themes/${SFEED_THEME}.h\" SFEED_CURSES_LDFLAGS = ${LDFLAGS} -lcurses # Linux: some distros use ncurses and require -lncurses. @@ -34,7 +34,7 @@ SFEED_CURSES_LDFLAGS = ${LDFLAGS} -lcurses # Gentoo Linux: some distros might also require -ltinfo and -D_DEFAULT_SOURCE # to prevent warnings about feature test macros. -#SFEED_CURSES_LDFLAGS = ${LDFLAGS} -lcurses -ltinfo +#SFEED_CURSES_LDFLAGS = ${LDFLAGS} -lncurses -ltinfo # FreeBSD: unset feature test macros for SIGWINCH etc. #SFEED_CURSES_CPPFLAGS = @@ -51,6 +51,7 @@ BIN = \ sfeed_frames\ sfeed_gopher\ sfeed_html\ + sfeed_json\ sfeed_mbox\ sfeed_opml_import\ sfeed_plain\ @@ -112,7 +113,7 @@ ${OBJ}: ${HDR} .c.o: ${CC} -o $@ -c $< ${SFEED_CFLAGS} ${SFEED_CPPFLAGS} -sfeed_curses.o: sfeed_curses.c +sfeed_curses.o: sfeed_curses.c themes/${SFEED_THEME}.h ${CC} -o $@ -c sfeed_curses.c ${SFEED_CURSES_CFLAGS} ${SFEED_CURSES_CPPFLAGS} sfeed_curses: ${LIB} sfeed_curses.o @@ -38,7 +38,7 @@ Initial setup: cp sfeedrc.example "$HOME/.sfeed/sfeedrc" Edit the sfeedrc(5) configuration file and change any RSS/Atom feeds. This file -is included and evaluated as a shellscript for sfeed_update, so it's functions +is included and evaluated as a shellscript for sfeed_update, so its functions and behaviour can be overridden: $EDITOR "$HOME/.sfeed/sfeedrc" @@ -76,6 +76,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 @@ -107,7 +108,8 @@ Optional dependencies - POSIX sh(1), used by sfeed_update(1) and sfeed_opml_export(1). - POSIX utilities such as awk(1) and sort(1), - used by sfeed_content(1), sfeed_markread(1) and sfeed_update(1). + used by sfeed_content(1), sfeed_markread(1), sfeed_opml_export(1) and + sfeed_update(1). - curl(1) binary: https://curl.haxx.se/ , used by sfeed_update(1), but can be replaced with any tool like wget(1), OpenBSD ftp(1) or hurl(1): https://git.codemadness.org/hurl/ @@ -115,6 +117,8 @@ Optional dependencies used by sfeed_update(1). If the text in your RSS/Atom feeds are already UTF-8 encoded then you don't need this. For a minimal iconv implementation: https://git.etalabs.net/cgit/noxcuse/tree/src/iconv.c +- xargs with support for the -P and -0 option, + used by sfeed_update(1). - mandoc for documentation: https://mdocml.bsd.lv/ - curses (typically ncurses), otherwise see minicurses.h, used by sfeed_curses(1). @@ -140,12 +144,12 @@ sfeed supports a subset of XML 1.0 and a subset of: - Atom 1.0 (RFC 4287): https://datatracker.ietf.org/doc/html/rfc4287 - Atom 0.3 (draft, historic). -- RSS 0.91+. +- RSS 0.90+. - RDF (when used with RSS). - MediaRSS extensions (media:). - Dublin Core extensions (dc:). -Other formats like JSONfeed, twtxt or certain RSS/Atom extensions can be +Other formats like JSON Feed, twtxt or certain RSS/Atom extensions are supported by converting them to RSS/Atom or to the sfeed(5) format directly. @@ -153,7 +157,7 @@ OS tested --------- - Linux, - compilers: clang, gcc, chibicc, cproc, lacc, pcc, tcc, + compilers: clang, gcc, chibicc, cproc, lacc, pcc, scc, tcc, libc: glibc, musl. - OpenBSD (clang, gcc). - NetBSD (with NetBSD curses). @@ -164,7 +168,7 @@ OS tested - Windows (cygwin gcc + mintty, mingw). - HaikuOS - SerenityOS -- FreeDOS (djgpp). +- FreeDOS (djgpp, Open Watcom). - FUZIX (sdcc -mz80, with the sfeed parser program). @@ -185,6 +189,7 @@ sfeed_curses - Format feed data (TSV) to a curses interface. sfeed_frames - Format feed data (TSV) to HTML file(s) with frames. sfeed_gopher - Format feed data (TSV) to Gopher files. sfeed_html - Format feed data (TSV) to HTML. +sfeed_json - Format feed data (TSV) to JSON Feed. sfeed_opml_export - Generate an OPML XML file from a sfeedrc config file. sfeed_opml_import - Generate a sfeedrc config file from an OPML XML file. sfeed_markread - Mark items as read/unread, for use with sfeed_curses. @@ -245,13 +250,13 @@ Find RSS/Atom feed URLs from a webpage: output example: - https://codemadness.org/blog/rss.xml application/rss+xml - https://codemadness.org/blog/atom.xml application/atom+xml + https://codemadness.org/atom.xml application/atom+xml + https://codemadness.org/atom_content.xml application/atom+xml - - - -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" @@ -287,10 +292,12 @@ Just like the other format programs included in sfeed you can run it like this: sfeed_curses < ~/.sfeed/feeds/xkcd -By default sfeed_curses marks the items of the last day as new/bold. To manage -read/unread items in a different way a plain-text file with a list of the read -URLs can be used. To enable this behaviour the path to this file can be -specified by setting the environment variable $SFEED_URL_FILE to the URL file: +By default sfeed_curses marks the items of the last day as new/bold. This limit +might be overridden by setting the environment variable $SFEED_NEW_AGE to the +desired maximum in seconds. To manage read/unread items in a different way a +plain-text file with a list of the read URLs can be used. To enable this +behaviour the path to this file can be specified by setting the environment +variable $SFEED_URL_FILE to the URL file: export SFEED_URL_FILE="$HOME/.sfeed/urls" [ -f "$SFEED_URL_FILE" ] || touch "$SFEED_URL_FILE" @@ -332,7 +339,7 @@ filtering items per feed. It can be used to shorten URLs, filter away advertisements, strip tracking parameters and more. # filter fields. - # filter(name) + # filter(name, url) filter() { case "$1" in "tweakers") @@ -578,7 +585,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 @@ -675,8 +682,8 @@ additional metadata from the previous request. CDNs blocking requests due to a missing HTTP User-Agent request header sfeed_update will not send the "User-Agent" header by default for privacy -reasons. Some CDNs like Cloudflare don't like this and will block such HTTP -requests. +reasons. Some CDNs like Cloudflare or websites like Reddit.com don't like this +and will block such HTTP requests. A custom User-Agent can be set by using the curl -H option, like so: @@ -701,56 +708,6 @@ sfeedrc file and change the curl options "-L --max-redirs 0". - - - -Shellscript to update feeds in parallel more efficiently using xargs -P. - -It creates a queue of the feeds with its settings, then uses xargs to process -them in parallel using the common, but non-POSIX -P option. This is more -efficient than the more portable solution in sfeed_update which can stall a -batch of $maxjobs in the queue if one item is slow. - -sfeed_update_xargs shellscript: - - #!/bin/sh - # update feeds, merge with old feeds using xargs in parallel mode (non-POSIX). - - # include script and reuse its functions, but do not start main(). - SFEED_UPDATE_INCLUDE="1" . sfeed_update - # load config file, sets $config. - loadconfig "$1" - - # process a single feed. - # args are: config, tmpdir, name, feedurl, basesiteurl, encoding - if [ "${SFEED_UPDATE_CHILD}" = "1" ]; then - sfeedtmpdir="$2" - _feed "$3" "$4" "$5" "$6" - exit $? - fi - - # ...else parent mode: - - # feed(name, feedurl, basesiteurl, encoding) - feed() { - # workaround: *BSD xargs doesn't handle empty fields in the middle. - name="${1:-$$}" - feedurl="${2:-http://}" - basesiteurl="${3:-${feedurl}}" - encoding="$4" - - printf '%s\0%s\0%s\0%s\0%s\0%s\0' "${config}" "${sfeedtmpdir}" \ - "${name}" "${feedurl}" "${basesiteurl}" "${encoding}" - } - - # fetch feeds and store in temporary directory. - sfeedtmpdir="$(mktemp -d '/tmp/sfeed_XXXXXX')" - # make sure path exists. - mkdir -p "${sfeedpath}" - # print feeds for parallel processing with xargs. - feeds | SFEED_UPDATE_CHILD="1" xargs -r -0 -P "${maxjobs}" -L 6 "$(readlink -f "$0")" - # cleanup temporary files etc. - cleanup - -- - - - Shellscript to handle URLs and enclosures in parallel using xargs -P. This can be used to download and process URLs for downloading podcasts, @@ -764,7 +721,7 @@ arguments are specified then the data is read from stdin. #!/bin/sh # sfeed_download: downloader for URLs and enclosures in sfeed(5) files. - # Dependencies: awk, curl, flock, xargs (-P), youtube-dl. + # Dependencies: awk, curl, flock, xargs (-P), yt-dlp. cachefile="${SFEED_CACHEFILE:-$HOME/.sfeed/downloaded_urls}" jobs="${SFEED_JOBS:-4}" @@ -777,14 +734,14 @@ arguments are specified then the data is read from stdin. else s="$2" fi - printf '[%s]: %s: %s\n' "$(date +'%H:%M:%S')" "${s}" "$3" >&2 + printf '[%s]: %s: %s\n' "$(date +'%H:%M:%S')" "${s}" "$3" } # fetch(url, feedname) fetch() { case "$1" in *youtube.com*) - youtube-dl "$1";; + yt-dlp "$1";; *.flac|*.ogg|*.m3u|*.m3u8|*.m4a|*.mkv|*.mp3|*.mp4|*.wav|*.webm) # allow 2 redirects, hide User-Agent, connect timeout is 15 seconds. curl -O -L --max-redirs 2 -H "User-Agent:" -f -s --connect-timeout 15 "$1";; @@ -803,14 +760,13 @@ arguments are specified then the data is read from stdin. if [ "${feedname}" != "-" ]; then mkdir -p "${feedname}" if ! cd "${feedname}"; then - log "${feedname}" "${msg}: ${feedname}" "DIR FAIL" - exit 1 + log "${feedname}" "${msg}: ${feedname}" "DIR FAIL" >&2 + return 1 fi fi log "${feedname}" "${msg}" "START" - fetch "${url}" "${feedname}" - if [ $? = 0 ]; then + if fetch "${url}" "${feedname}"; then log "${feedname}" "${msg}" "OK" # append it safely in parallel to the cachefile on a @@ -819,21 +775,23 @@ arguments are specified then the data is read from stdin. printf '%s\n' "${url}" >> "${cachefile}" ) 9>"${lockfile}" else - log "${feedname}" "${msg}" "FAIL" + log "${feedname}" "${msg}" "FAIL" >&2 + return 1 fi + return 0 } 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 # ...else parent mode: - tmp=$(mktemp) + tmp="$(mktemp)" || exit 1 trap "rm -f ${tmp}" EXIT [ -f "${cachefile}" ] || touch "${cachefile}" @@ -963,8 +921,199 @@ TSV format. - - - -Running custom commands inside the program ------------------------------------------- +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 +------------------------------- + +It can be useful to show the counts of unread items, for example in a +windowmanager or statusbar. + +The below example script counts the items of the last day in the same way the +formatting tools do: + + #!/bin/sh + # Count the new items of the last day. + LC_ALL=C awk -F '\t' -v "old=$(($(date +'%s') - 86400))" ' + { + total++; + } + int($1) >= old { + totalnew++; + } + END { + print "New: " totalnew; + print "Total: " total; + }' ~/.sfeed/feeds/* + +The below example script counts the unread items using the sfeed_curses URL +file: + + #!/bin/sh + # Count the unread and total items from feeds using the URL file. + LC_ALL=C awk -F '\t' ' + # URL file: amount of fields is 1. + NF == 1 { + u[$0] = 1; # lookup table of URLs. + next; + } + # feed file: check by URL or id. + { + total++; + if (length($3)) { + if (u[$3]) + read++; + } else if (length($6)) { + if (u[$6]) + read++; + } + } + END { + print "Unread: " (total - read); + print "Total: " total; + }' ~/.sfeed/urls ~/.sfeed/feeds/* + +- - - + +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 +------------------------------------------------------- Running commands inside the sfeed_curses program can be useful for example to sync items or mark all items across all feeds as read. It can be comfortable to @@ -983,14 +1132,13 @@ or forkexec((char *[]) { "syncnews.sh", NULL }, 1); break; -The specified script should be in $PATH or an absolute path. +The specified script should be in $PATH or be an absolute path. Example of a `markallread.sh` shellscript to mark all URLs as read: #!/bin/sh # mark all items/URLs as read. - - tmp=$(mktemp) + tmp="$(mktemp)" || exit 1 (cat ~/.sfeed/urls; cut -f 3 ~/.sfeed/feeds/*) | \ awk '!x[$0]++' > "$tmp" && mv "$tmp" ~/.sfeed/urls && @@ -999,7 +1147,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 @@ -1030,6 +1194,9 @@ testing sfeed_curses. Some of them might be fixed already upstream: middle-button, right-button is incorrect / reversed. - putty: the full reset attribute (ESC c, typically `rs1`) does not reset the window title. +- Mouse button encoding for extended buttons (like side-buttons) in some + terminals are unsupported or map to the same button: for example side-buttons 7 + and 8 map to the scroll buttons 4 and 5 in urxvt. License @@ -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 index ad24b5e..9b8112b 100644 --- a/minicurses.h +++ b/minicurses.h @@ -1,5 +1,3 @@ -#include <sys/ioctl.h> - #undef OK #define OK (0) @@ -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 , @@ -101,5 +111,5 @@ The README file has more examples. .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. +If a timezone for the timestamp field is not in the RFC 822 or RFC 3339 format +it is not supported and the timezone is interpreted as UTC+0. @@ -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. @@ -1,4 +1,3 @@ -#include <ctype.h> #include <errno.h> #include <stdint.h> #include <stdio.h> @@ -127,7 +126,7 @@ static void xmltagstartparsed(XMLParser *, const char *, size_t, int); /* map tag name to TagId type */ /* RSS, must be alphabetical order */ -static FeedTag rsstags[] = { +static const FeedTag rsstags[] = { { STRP("author"), RSSTagAuthor }, { STRP("category"), RSSTagCategory }, { STRP("content:encoded"), RSSTagContentEncoded }, @@ -144,7 +143,7 @@ static FeedTag rsstags[] = { }; /* Atom, must be alphabetical order */ -static FeedTag atomtags[] = { +static const FeedTag atomtags[] = { { STRP("author"), AtomTagAuthor }, { STRP("category"), AtomTagCategory }, { STRP("content"), AtomTagContent }, @@ -161,14 +160,14 @@ static FeedTag atomtags[] = { }; /* special case: nested <author><name> */ -static FeedTag atomtagauthor = { STRP("author"), AtomTagAuthor }; -static FeedTag atomtagauthorname = { STRP("name"), AtomTagAuthorName }; +static const FeedTag atomtagauthor = { STRP("author"), AtomTagAuthor }; +static const FeedTag atomtagauthorname = { STRP("name"), AtomTagAuthorName }; /* reference to no / unknown tag */ -static FeedTag notag = { STRP(""), TagUnknown }; +static const FeedTag notag = { STRP(""), TagUnknown }; /* map TagId type to RSS/Atom field, all tags must be defined */ -static int fieldmap[TagLast] = { +static const int fieldmap[TagLast] = { [TagUnknown] = -1, /* RSS */ [RSSTagDcdate] = FeedFieldTime, @@ -205,7 +204,7 @@ static int fieldmap[TagLast] = { static const int FieldSeparator = '\t'; /* separator for multiple values in a field, separator should be 1 byte */ -static const char *FieldMultiSeparator = "|"; +static const char FieldMultiSeparator[] = "|"; static struct uri baseuri; static const char *baseurl; @@ -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; } @@ -294,7 +293,7 @@ string_append(String *s, const char *data, size_t len) return; if (s->len >= SIZE_MAX - len) { - errno = EOVERFLOW; + errno = ENOMEM; err(1, "realloc"); } @@ -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); } @@ -384,7 +383,7 @@ string_print_trimmed_multi(String *s) } } -/* Print URL, if it's a relative URL then it uses the global `baseurl`. */ +/* Print URL, if it is a relative URL then it uses the global `baseurl`. */ static void printuri(char *s) { @@ -410,7 +409,7 @@ printuri(char *s) *e = c; /* restore NUL byte to original character */ } -/* Print URL, if it's a relative URL then it uses the global `baseurl`. */ +/* Print URL, if it is a relative URL then it uses the global `baseurl`. */ static void string_print_uri(String *s) { @@ -433,18 +432,23 @@ string_print_timestamp(String *s) printf("%lld", t); } -/* Convert time fields. Returns a UNIX timestamp. */ +/* Convert time fields. Returns a signed (at least) 64-bit UNIX timestamp. + Parameters should be passed as they are in a struct tm: + that is: year = year - 1900, month = month - 1. */ static long long datetounix(long long year, int mon, int day, int hour, int min, int sec) { - static const int secs_through_month[] = { + /* seconds in a month in a regular (non-leap) year */ + static const long secs_through_month[] = { 0, 31 * 86400, 59 * 86400, 90 * 86400, 120 * 86400, 151 * 86400, 181 * 86400, 212 * 86400, 243 * 86400, 273 * 86400, 304 * 86400, 334 * 86400 }; int is_leap = 0, cycles, centuries = 0, leaps = 0, rem; long long t; + /* optimization: handle common range year 1902 up to and including 2038 */ if (year - 2ULL <= 136) { + /* amount of leap days relative to 1970: every 4 years */ leaps = (year - 68) >> 2; if (!((year - 68) & 3)) { leaps--; @@ -452,8 +456,11 @@ datetounix(long long year, int mon, int day, int hour, int min, int sec) } else { is_leap = 0; } - t = 31536000 * (year - 70) + 86400 * leaps; + t = 31536000 * (year - 70) + (86400 * leaps); /* 365 * 86400 = 31536000 */ } else { + /* general leap year calculation: + leap years occur mostly every 4 years but every 100 years + a leap year is skipped unless the year is divisible by 400 */ cycles = (year - 100) / 400; rem = (year - 100) % 400; if (rem < 0) { @@ -463,20 +470,27 @@ datetounix(long long year, int mon, int day, int hour, int min, int sec) if (!rem) { is_leap = 1; } else { - if (rem >= 300) - centuries = 3, rem -= 300; - else if (rem >= 200) - centuries = 2, rem -= 200; - else if (rem >= 100) - centuries = 1, rem -= 100; + if (rem >= 300) { + centuries = 3; + rem -= 300; + } else if (rem >= 200) { + centuries = 2; + rem -= 200; + } else if (rem >= 100) { + centuries = 1; + rem -= 100; + } if (rem) { leaps = rem / 4U; rem %= 4U; is_leap = !rem; } } - leaps += 97 * cycles + 24 * centuries - is_leap; - t = (year - 100) * 31536000LL + leaps * 86400LL + 946684800 + 86400; + leaps += (97 * cycles) + (24 * centuries) - is_leap; + + /* adjust 8 leap days from 1970 up to and including 2000: + ((30 * 365) + 8) * 86400 = 946771200 */ + t = ((year - 100) * 31536000LL) + (leaps * 86400LL) + 946771200LL; } t += secs_through_month[mon]; if (is_leap && mon >= 2) @@ -490,16 +504,16 @@ datetounix(long long year, int mon, int day, int hour, int min, int sec) } /* Get timezone from string, return time offset in seconds from UTC. - * NOTE: only parses timezones in RFC-822, many other timezone names are + * NOTE: only parses timezones in RFC 822, many other timezone names are * ambiguous anyway. - * ANSI and military zones are defined wrong in RFC822 and are unsupported, - * see note on RFC2822 4.3 page 32. */ + * ANSI and military zones are defined wrong in RFC 822 and are unsupported, + * see note on RFC 2822 4.3 page 32. */ static long gettzoffset(const char *s) { - static struct { + static const struct { char *name; - const int offhour; + int offhour; } tzones[] = { { "CDT", -5 * 3600 }, { "CST", -6 * 3600 }, @@ -514,24 +528,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; @@ -545,7 +559,7 @@ gettzoffset(const char *s) static int parsetime(const char *s, long long *tp) { - static struct { + static const struct { char *name; int len; } mons[] = { @@ -565,35 +579,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 +623,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; + /* obsolete short year: RFC 2822 4.3 */ + if (i == 2 || i == 3) + v += (i == 2 && 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 +640,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' || *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++) ; } @@ -679,6 +693,9 @@ printfields(void) putchar(FieldSeparator); string_print_trimmed_multi(&ctx.fields[FeedFieldCategory].str); putchar('\n'); + + if (ferror(stdout)) /* check for errors but do not flush */ + checkfileerror(stdout, "<stdout>", 'w'); } static int @@ -707,8 +724,8 @@ xmlattr(XMLParser *p, const char *t, size_t tl, const char *n, size_t nl, if (!ctx.tag.id) return; - /* content-type may be: Atom: text, xhtml, html or mime-type. - MRSS (media:description): plain, html. */ + /* content-type may be for Atom: text, xhtml, html or a mime-type. + for MRSS (media:description): plain, html. */ if (ISCONTENTTAG(ctx)) { if (isattr(n, nl, STRP("type"))) string_append(&attrtype, v, vl); @@ -741,7 +758,7 @@ static void xmlattrentity(XMLParser *p, const char *t, size_t tl, const char *n, size_t nl, const char *data, size_t datalen) { - char buf[16]; + char buf[8]; int len; /* handles transforming inline XML to data */ @@ -755,7 +772,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 @@ -818,7 +835,7 @@ xmldata(XMLParser *p, const char *s, size_t len) static void xmldataentity(XMLParser *p, const char *data, size_t datalen) { - char buf[16]; + char buf[8]; int len; if (!ctx.field) @@ -835,7 +852,7 @@ xmldataentity(XMLParser *p, const char *data, size_t datalen) static void xmltagstart(XMLParser *p, const char *t, size_t tl) { - FeedTag *f; + const FeedTag *f; if (ISINCONTENT(ctx)) { if (ctx.contenttype == ContentTypeHTML) { @@ -894,7 +911,7 @@ xmltagstartparsed(XMLParser *p, const char *t, size_t tl, int isshort) return; } - /* set tag type based on it's attribute value */ + /* set tag type based on its attribute value */ if (ctx.tag.id == RSSTagGuid) { /* if empty the default is "true" */ if (!attrispermalink.len || @@ -964,7 +981,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); @@ -976,7 +993,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); @@ -1005,7 +1022,7 @@ xmltagend(XMLParser *p, const char *t, size_t tl, int isshort) } /* temporary string: for fields that cannot be processed - directly and need more context, for example by it's tag + directly and need more context, for example by its tag attributes, like the Atom link rel="alternate|enclosure". */ if (tmpstr.len && ctx.field) { if (ISFEEDFIELDMULTI(fieldmap[ctx.tag.id])) { @@ -1056,8 +1073,11 @@ main(int argc, char *argv[]) parser.xmltagstart = xmltagstart; parser.xmltagstartparsed = xmltagstartparsed; - /* NOTE: getnext is defined in xml.h for inline optimization */ + /* NOTE: GETNEXT is defined in xml.h for inline optimization */ xml_parse(&parser); + checkfileerror(stdin, "<stdin>", 'r'); + checkfileerror(stdout, "<stdout>", 'w'); + return 0; } diff --git a/sfeed_atom.c b/sfeed_atom.c index 1dff75b..d0b139d 100644 --- a/sfeed_atom.c +++ b/sfeed_atom.c @@ -1,5 +1,3 @@ -#include <sys/types.h> - #include <stdio.h> #include <string.h> #include <time.h> @@ -22,6 +20,8 @@ printcontent(const char *s) case '&': fputs("&", stdout); break; case '"': fputs(""", stdout); break; case '\\': + if (*(s + 1) == '\0') + break; s++; switch (*s) { case 'n': putchar('\n'); break; @@ -43,7 +43,8 @@ printfeed(FILE *fp, const char *feedname) ssize_t linelen; int c; - while ((linelen = getline(&line, &linesize, fp)) > 0) { + while ((linelen = getline(&line, &linesize, fp)) > 0 && + !ferror(stdout)) { if (line[linelen - 1] == '\n') line[--linelen] = '\0'; parseline(line, fields); @@ -94,7 +95,7 @@ printfeed(FILE *fp, const char *feedname) /* NOTE: an RSS/Atom viewer may or may not format whitespace such as newlines. Workaround: type="html" and <![CDATA[<pre></pre>]]> */ - fputs("\t<content type=\"text\">", stdout); + fputs("\t<content>", stdout); } printcontent(fields[FieldContent]); fputs("</content>\n", stdout); @@ -124,15 +125,14 @@ main(int argc, char *argv[]) if (pledge(argc == 1 ? "stdio" : "stdio rpath", NULL) == -1) err(1, "pledge"); - if ((now = time(NULL)) == -1) - err(1, "time"); + if ((now = time(NULL)) == (time_t)-1) + errx(1, "time"); if (!(tm = gmtime_r(&now, &tmnow))) - err(1, "gmtime_r"); + err(1, "gmtime_r: can't get current time"); fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n" - "\t<title type=\"text\">Newsfeed</title>\n" - "\t<author><name>sfeed</name></author>\n", stdout); + "\t<title>Newsfeed</title>\n", stdout); printf("\t<id>urn:newsfeed:%lld</id>\n" "\t<updated>%04d-%02d-%02dT%02d:%02d:%02dZ</updated>\n", (long long)now, @@ -141,19 +141,22 @@ main(int argc, char *argv[]) if (argc == 1) { printfeed(stdin, ""); + checkfileerror(stdin, "<stdin>", 'r'); } else { for (i = 1; i < argc; i++) { if (!(fp = fopen(argv[i], "r"))) err(1, "fopen: %s", argv[i]); name = ((name = strrchr(argv[i], '/'))) ? name + 1 : argv[i]; printfeed(fp, name); - if (ferror(fp)) - err(1, "ferror: %s", argv[i]); + checkfileerror(fp, argv[i], 'r'); + checkfileerror(stdout, "<stdout>", 'w'); fclose(fp); } } fputs("</feed>\n", stdout); + checkfileerror(stdout, "<stdout>", 'w'); + return 0; } diff --git a/sfeed_curses.1 b/sfeed_curses.1 index f507475..d40d453 100644 --- a/sfeed_curses.1 +++ b/sfeed_curses.1 @@ -1,4 +1,4 @@ -.Dd February 4, 2022 +.Dd August 1, 2023 .Dt SFEED_CURSES 1 .Os .Sh NAME @@ -30,6 +30,7 @@ arguments are specified then the data is read from stdin and the feed name is .Pp Items with a timestamp from the last day compared to the system time at the time of loading the feed are marked as new and bold. +This value might be overridden through environment variables. There is also an alternative mode available to mark items as read by matching it against a list of URLs from a plain-text file. Items with an enclosure are marked with a @ symbol. @@ -83,6 +84,9 @@ Go to the next feed in the feeds pane and open it. Redraw screen. .It R 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. .It m Toggle mouse-mode. It supports xterm X10 and extended SGR encoding. @@ -95,11 +99,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. @@ -137,12 +141,12 @@ This will only work when .Ev SFEED_URL_FILE is set. .It f -Mark all items of the current loaded feed as read. +Mark all items of the currently loaded feed as read. This will only work when .Ev SFEED_URL_FILE is set. .It F -Mark all items of the current loaded feed as unread. +Mark all items of the currently loaded feed as unread. This will only work when .Ev SFEED_URL_FILE is set. @@ -182,40 +186,43 @@ Switch to the feeds pane. .Bl -tag -width Ds .It SIGHUP 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 Read and process a sequence of keys as input commands from this environment variable first, afterwards it reads from the tty as usual. This can be useful to automate certain actions at the start. +.It Ev SFEED_NEW_AGE +Overwrite the maximum age in seconds to mark feeds as new. +By default this is 86400, which equals one day. .It Ev SFEED_PIPER 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 @@ -223,10 +230,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, @@ -261,10 +264,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 @@ -273,8 +276,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/* @@ -293,24 +316,26 @@ sfeed_curses ~/.sfeed/feeds/* Which does the following: .Bl -enum .It -Set the current layout to a horizontal mode ('2' keybind'). +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 -Load the current selected feed ('o' keybind'). +.Pp +Load the currently 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 index d12f170..3359340 100644 --- a/sfeed_curses.c +++ b/sfeed_curses.c @@ -1,10 +1,7 @@ #include <sys/ioctl.h> #include <sys/select.h> -#include <sys/time.h> -#include <sys/types.h> #include <sys/wait.h> -#include <ctype.h> #include <errno.h> #include <fcntl.h> #include <locale.h> @@ -38,10 +35,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,24 +127,30 @@ 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); -void cleanup(void); -void draw(void); -int getsidebarsize(void); -void markread(struct pane *, off_t, off_t, int); -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); +static void alldirty(void); +static void cleanup(void); +static void draw(void); +static int getsidebarsize(void); +static void markread(struct pane *, off_t, off_t, int); +static void pane_draw(struct pane *); +static void sighandler(int); +static void updategeom(void); +static void updatesidebar(void); +static void urls_free(struct urls *); +static int urls_hasmatch(struct urls *, const char *); +static void urls_read(struct urls *, const char *); static struct linebar linebar; static struct statusbar statusbar; @@ -170,10 +173,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; +static 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 */ @@ -186,7 +190,7 @@ static int piperia = 1; /* env variable: $SFEED_PIPER_INTERACTIVE */ static int yankeria = 0; /* env variable: $SFEED_YANKER_INTERACTIVE */ static int lazyload = 0; /* env variable: $SFEED_LAZYLOAD */ -int +static int ttywritef(const char *fmt, ...) { va_list ap; @@ -200,7 +204,7 @@ ttywritef(const char *fmt, ...) return n; } -int +static int ttywrite(const char *s) { if (!s) @@ -208,13 +212,8 @@ 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 +__dead static void die(const char *fmt, ...) { va_list ap; @@ -229,13 +228,13 @@ die(const char *fmt, ...) if (saved_errno) fprintf(stderr, ": %s", strerror(saved_errno)); + putc('\n', stderr); fflush(stderr); - write(2, "\n", 1); _exit(1); } -void * +static void * erealloc(void *ptr, size_t size) { void *p; @@ -245,7 +244,7 @@ erealloc(void *ptr, size_t size) return p; } -void * +static void * ecalloc(size_t nmemb, size_t size) { void *p; @@ -255,7 +254,7 @@ ecalloc(size_t nmemb, size_t size) return p; } -char * +static char * estrdup(const char *s) { char *p; @@ -266,7 +265,7 @@ estrdup(const char *s) } /* Wrapper for tparm() which allows NULL parameter for str. */ -char * +static char * tparmnull(const char *str, long p1, long p2, long p3, long p4, long p5, long p6, long p7, long p8, long p9) { @@ -277,7 +276,7 @@ tparmnull(const char *str, long p1, long p2, long p3, long p4, long p5, long p6, } /* Counts column width of character string. */ -size_t +static size_t colw(const char *s) { wchar_t wc; @@ -309,7 +308,7 @@ colw(const char *s) /* Format `len` columns of characters. If string is shorter pad the rest with characters `pad`. */ -int +static int utf8pad(char *buf, size_t bufsiz, const char *s, size_t len, int pad) { wchar_t wc; @@ -374,13 +373,13 @@ utf8pad(char *buf, size_t bufsiz, const char *s, size_t len, int pad) return 0; } -void +static void resetstate(void) { ttywrite("\x1b""c"); /* rs1: reset title and state */ } -void +static void updatetitle(void) { unsigned long totalnew = 0, total = 0; @@ -393,32 +392,32 @@ updatetitle(void) ttywritef("\x1b]2;(%lu/%lu) - sfeed_curses\x1b\\", totalnew, total); } -void +static void appmode(int on) { ttywrite(tparmnull(on ? enter_ca_mode : exit_ca_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0)); } -void +static void mousemode(int on) { ttywrite(on ? "\x1b[?1000h" : "\x1b[?1000l"); /* xterm X10 mouse mode */ ttywrite(on ? "\x1b[?1006h" : "\x1b[?1006l"); /* extended SGR mouse mode */ } -void +static void cursormode(int on) { ttywrite(tparmnull(on ? cursor_normal : cursor_invisible, 0, 0, 0, 0, 0, 0, 0, 0, 0)); } -void +static void cursormove(int x, int y) { ttywrite(tparmnull(cursor_address, y, x, 0, 0, 0, 0, 0, 0, 0)); } -void +static void cursorsave(void) { /* do not save the cursor if it won't be restored anyway */ @@ -426,7 +425,7 @@ cursorsave(void) ttywrite(tparmnull(save_cursor, 0, 0, 0, 0, 0, 0, 0, 0, 0)); } -void +static void cursorrestore(void) { /* if the cursor cannot be hidden then move to a consistent position */ @@ -436,7 +435,7 @@ cursorrestore(void) cursormove(0, 0); } -void +static void attrmode(int mode) { switch (mode) { @@ -457,19 +456,19 @@ attrmode(int mode) } } -void +static void cleareol(void) { ttywrite(tparmnull(clr_eol, 0, 0, 0, 0, 0, 0, 0, 0, 0)); } -void +static void clearscreen(void) { ttywrite(tparmnull(clear_screen, 0, 0, 0, 0, 0, 0, 0, 0, 0)); } -void +static void cleanup(void) { struct sigaction sa; @@ -498,7 +497,7 @@ cleanup(void) sigaction(SIGWINCH, &sa, NULL); } -void +static void win_update(struct win *w, int width, int height) { if (width != w->width || height != w->height) @@ -507,7 +506,7 @@ win_update(struct win *w, int width, int height) w->height = height; } -void +static void resizewin(void) { struct winsize winsz; @@ -522,7 +521,7 @@ resizewin(void) alldirty(); } -void +static void init(void) { struct sigaction sa; @@ -549,40 +548,44 @@ 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); sigaction(SIGWINCH, &sa, NULL); } -void +static 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); } } @@ -591,7 +594,7 @@ processexit(pid_t pid, int interactive) if `interactive` is 1 then cleanup and restore the tty and wait on the process. if 0 then don't do that and also write stdout and stderr to /dev/null. */ -void +static void pipeitem(const char *cmd, struct item *item, int field, int interactive) { FILE *fp; @@ -606,8 +609,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; @@ -631,7 +634,7 @@ pipeitem(const char *cmd, struct item *item, int field, int interactive) } } -void +static void forkexec(char *argv[], int interactive) { pid_t pid; @@ -644,8 +647,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); @@ -654,7 +658,7 @@ forkexec(char *argv[], int interactive) } } -struct row * +static struct row * pane_row_get(struct pane *p, off_t pos) { if (pos < 0 || pos >= p->nrows) @@ -665,7 +669,7 @@ pane_row_get(struct pane *p, off_t pos) return p->rows + pos; } -char * +static char * pane_row_text(struct pane *p, struct row *row) { /* custom formatter */ @@ -674,7 +678,7 @@ pane_row_text(struct pane *p, struct row *row) return row->text; } -int +static int pane_row_match(struct pane *p, struct row *row, const char *s) { if (p->row_match) @@ -682,7 +686,7 @@ pane_row_match(struct pane *p, struct row *row, const char *s) return (strcasestr(pane_row_text(p, row), s) != NULL); } -void +static void pane_row_draw(struct pane *p, off_t pos, int selected) { struct row *row; @@ -715,7 +719,7 @@ pane_row_draw(struct pane *p, off_t pos, int selected) cursorrestore(); } -void +static void pane_setpos(struct pane *p, off_t pos) { if (pos < 0) @@ -739,7 +743,7 @@ pane_setpos(struct pane *p, off_t pos) p->pos = pos; } -void +static void pane_scrollpage(struct pane *p, int pages) { off_t pos; @@ -757,13 +761,13 @@ pane_scrollpage(struct pane *p, int pages) } } -void +static void pane_scrolln(struct pane *p, int n) { pane_setpos(p, p->pos + n); } -void +static void pane_setfocus(struct pane *p, int on) { if (p->focused != on) { @@ -772,7 +776,7 @@ pane_setfocus(struct pane *p, int on) } } -void +static void pane_draw(struct pane *p) { off_t pos, y; @@ -789,7 +793,7 @@ pane_draw(struct pane *p) pane_row_draw(p, y + pos, (y + pos) == p->pos); } -void +static void setlayout(int n) { if (layout != LayoutMonocle) @@ -797,7 +801,7 @@ setlayout(int n) layout = n; } -void +static void updategeom(void) { int h, w, x = 0, y = 0; @@ -871,7 +875,7 @@ updategeom(void) alldirty(); } -void +static void scrollbar_setfocus(struct scrollbar *s, int on) { if (s->focused != on) { @@ -880,7 +884,7 @@ scrollbar_setfocus(struct scrollbar *s, int on) } } -void +static void scrollbar_update(struct scrollbar *s, off_t pos, off_t nrows, int pageheight) { int tickpos = 0, ticksize = 0; @@ -905,7 +909,7 @@ scrollbar_update(struct scrollbar *s, off_t pos, off_t nrows, int pageheight) s->ticksize = ticksize; } -void +static void scrollbar_draw(struct scrollbar *s) { off_t y; @@ -944,15 +948,17 @@ scrollbar_draw(struct scrollbar *s) cursorrestore(); } -int +static int readch(void) { unsigned char b; fd_set readfds; struct timeval tv; - if (cmdenv && *cmdenv) - return *(cmdenv++); + if (cmdenv && *cmdenv) { + b = *(cmdenv++); /* $SFEED_AUTOCMD */ + return (int)b; + } for (;;) { FD_ZERO(&readfds); @@ -976,15 +982,17 @@ readch(void) } } -char * +static char * lineeditor(void) { char *input = NULL; size_t cap = 0, nchars = 0; int ch; + if (usemouse) + mousemode(0); for (;;) { - if (nchars + 1 >= cap) { + if (nchars + 2 >= cap) { cap = cap ? cap * 2 : 32; input = erealloc(input, cap); } @@ -997,34 +1005,37 @@ lineeditor(void) if (!nchars) continue; input[--nchars] = '\0'; - write(1, "\b \b", 3); /* back, blank, back */ - continue; + ttywrite("\b \b"); /* back, blank, back */ } else if (ch >= ' ') { input[nchars] = ch; - write(1, &input[nchars], 1); + input[nchars + 1] = '\0'; + 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; } -char * +static char * uiprompt(int x, int y, char *fmt, ...) { va_list ap; @@ -1054,7 +1065,7 @@ uiprompt(int x, int y, char *fmt, ...) return input; } -void +static void linebar_draw(struct linebar *b) { int i; @@ -1075,7 +1086,7 @@ linebar_draw(struct linebar *b) cursorrestore(); } -void +static void statusbar_draw(struct statusbar *s) { if (!s->dirty) @@ -1095,7 +1106,7 @@ statusbar_draw(struct statusbar *s) cursorrestore(); } -void +static void statusbar_update(struct statusbar *s, const char *text) { if (s->text && !strcmp(s->text, text)) @@ -1107,7 +1118,7 @@ statusbar_update(struct statusbar *s, const char *text) } /* Line to item, modifies and splits line in-place. */ -int +static int linetoitem(char *line, struct item *item) { char *fields[FieldLast]; @@ -1133,7 +1144,7 @@ linetoitem(char *line, struct item *item) return 0; } -void +static void feed_items_free(struct items *items) { size_t i; @@ -1148,7 +1159,7 @@ feed_items_free(struct items *items) items->cap = 0; } -void +static void feed_items_get(struct feed *f, FILE *fp, struct items *itemsret) { struct item *item, *items = NULL; @@ -1190,13 +1201,13 @@ 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); } -void +static void updatenewitems(struct feed *f) { struct pane *p; @@ -1205,12 +1216,13 @@ updatenewitems(struct feed *f) size_t i; p = &panes[PaneItems]; + p->dirty = 1; f->totalnew = 0; for (i = 0; i < p->nrows; i++) { 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; @@ -1219,7 +1231,7 @@ updatenewitems(struct feed *f) f->total = p->nrows; } -void +static void feed_load(struct feed *f, FILE *fp) { /* static, reuse local buffers */ @@ -1238,11 +1250,9 @@ feed_load(struct feed *f, FILE *fp) p->rows[i].data = &(items.items[i]); /* do not use pane_row_get() */ updatenewitems(f); - - p->dirty = 1; } -void +static void feed_count(struct feed *f, FILE *fp) { char *fields[FieldLast]; @@ -1258,7 +1268,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)) @@ -1271,7 +1281,7 @@ feed_count(struct feed *f, FILE *fp) free(line); } -void +static void feed_setenv(struct feed *f) { if (f && f->path) @@ -1281,7 +1291,7 @@ feed_setenv(struct feed *f) } /* Change feed, have one file open, reopen file if needed. */ -void +static void feeds_set(struct feed *f) { if (curfeed) { @@ -1301,16 +1311,15 @@ feeds_set(struct feed *f) curfeed = f; } -void +static void feeds_load(struct feed *feeds, size_t nfeeds) { struct feed *f; size_t i; - if ((comparetime = time(NULL)) == -1) - die("time"); - /* 1 day is old news */ - comparetime -= 86400; + errno = 0; + if ((comparetime = getcomparetime()) == (time_t)-1) + die("getcomparetime"); for (i = 0; i < nfeeds; i++) { f = &feeds[i]; @@ -1345,7 +1354,7 @@ feeds_load(struct feed *feeds, size_t nfeeds) } /* find row position of the feed if visible, else return -1 */ -off_t +static off_t feeds_row_get(struct pane *p, struct feed *f) { struct row *row; @@ -1362,7 +1371,7 @@ feeds_row_get(struct pane *p, struct feed *f) return -1; } -void +static void feeds_reloadall(void) { struct pane *p; @@ -1376,9 +1385,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(); @@ -1391,7 +1400,7 @@ feeds_reloadall(void) pane_setpos(p, 0); } -void +static void feed_open_selected(struct pane *p) { struct feed *f; @@ -1401,10 +1410,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(); @@ -1415,22 +1424,24 @@ feed_open_selected(struct pane *p) } } -void +static void 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); } -void +static void feed_pipe_selected_item(struct pane *p) { struct row *row; @@ -1443,7 +1454,7 @@ feed_pipe_selected_item(struct pane *p) pipeitem(pipercmd, item, -1, piperia); } -void +static void feed_yank_selected_item(struct pane *p, int field) { struct row *row; @@ -1456,7 +1467,7 @@ feed_yank_selected_item(struct pane *p, int field) } /* calculate optimal (default) size */ -int +static int getsidebarsizedefault(void) { struct feed *feed; @@ -1489,7 +1500,7 @@ getsidebarsizedefault(void) return 0; } -int +static int getsidebarsize(void) { int size; @@ -1499,7 +1510,7 @@ getsidebarsize(void) return size; } -void +static void adjustsidebarsize(int n) { int size; @@ -1522,7 +1533,7 @@ adjustsidebarsize(int n) } } -void +static void updatesidebar(void) { struct pane *p; @@ -1574,22 +1585,19 @@ updatesidebar(void) p->pos = p->nrows - 1; } -void +static 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; } } -void +static void alldirty(void) { win.dirty = 1; @@ -1601,7 +1609,7 @@ alldirty(void) statusbar.dirty = 1; } -void +static void draw(void) { struct row *row; @@ -1635,7 +1643,7 @@ draw(void) statusbar_draw(&statusbar); } -void +static void mousereport(int button, int release, int keymask, int x, int y) { struct pane *p; @@ -1707,7 +1715,7 @@ mousereport(int button, int release, int keymask, int x, int y) } /* Custom formatter for feed row. */ -char * +static char * feed_row_format(struct pane *p, struct row *row) { /* static, reuse local buffers */ @@ -1748,7 +1756,7 @@ feed_row_format(struct pane *p, struct row *row) return text; } -int +static int feed_row_match(struct pane *p, struct row *row, const char *s) { struct feed *feed; @@ -1758,7 +1766,7 @@ feed_row_match(struct pane *p, struct row *row, const char *s) return (strcasestr(feed->name, s) != NULL); } -struct row * +static struct row * item_row_get(struct pane *p, off_t pos) { struct row *itemrow; @@ -1779,6 +1787,7 @@ item_row_get(struct pane *p, off_t pos) if ((linelen = getline(&line, &linesize, f->fp)) <= 0) { if (ferror(f->fp)) die("getline: %s", f->path); + free(line); return NULL; } @@ -1794,7 +1803,7 @@ item_row_get(struct pane *p, off_t pos) } /* Custom formatter for item row. */ -char * +static char * item_row_format(struct pane *p, struct row *row) { /* static, reuse local buffers */ @@ -1826,7 +1835,7 @@ item_row_format(struct pane *p, struct row *row) return text; } -void +static void markread(struct pane *p, off_t from, off_t to, int isread) { struct row *row; @@ -1834,7 +1843,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; @@ -1845,8 +1854,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"))) @@ -1865,11 +1874,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 the exit status code was unavailable or non-zero */ + if (waitpid(pid, &status, 0) <= 0 || status) break; visstart = p->pos - (p->pos % p->height); /* visible start */ @@ -1891,38 +1898,41 @@ markread(struct pane *p, off_t from, off_t to, int isread) } } -int +static int urls_cmp(const void *v1, const void *v2) { return strcmp(*((char **)v1), *((char **)v2)); } -int -urls_isnew(const char *url) +static 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) +static 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) +static 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; @@ -1932,19 +1942,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 @@ -2012,9 +2022,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) @@ -2106,30 +2116,43 @@ 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 left */ - case 'D': goto keyleft; /* arrow right */ + 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 '4': /* end */ - if ((ch = readch()) < 0) - goto event; - if (ch == '~') - goto endpos; - continue; - case '5': /* page up */ - if ((ch = readch()) < 0) - goto event; - if (ch == '~') - goto prevpage; - continue; - case '6': /* page down */ - if ((ch = readch()) < 0) - goto event; - if (ch == '~') - goto nextpage; - continue; + case 'I': goto prevpage; /* page up */ + default: + if (!(ch >= '0' && ch <= '9')) + break; + for (i = ch - '0'; ;) { + if ((ch = readch()) < 0) { + goto event; + } else if (ch >= '0' && ch <= '9') { + i = (i * 10) + (ch - '0'); + continue; + } else if (ch == '~') { /* DEC: ESC [ num ~ */ + switch (i) { + case 1: goto startpos; /* home */ + case 4: goto endpos; /* end */ + case 5: goto prevpage; /* page up */ + case 6: goto nextpage; /* page down */ + 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; + } } break; keyup: @@ -2314,6 +2337,7 @@ nextpage: if (selpane == PaneItems && panes[selpane].nrows) { p = &panes[selpane]; markread(p, p->pos, p->pos, ch == 'r'); + pane_scrolln(&panes[selpane], +1); } break; case 's': /* toggle layout between monocle or non-monocle */ @@ -2332,23 +2356,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 + SIGTERM); + } + if (state_sigint) { cleanup(); - _exit(128 + sigstate); - case SIGWINCH: + _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_frames.1 b/sfeed_frames.1 index e29498b..455c1db 100644 --- a/sfeed_frames.1 +++ b/sfeed_frames.1 @@ -23,6 +23,7 @@ file is not written. 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 using a bold markup. +This value might be overridden through environment variables. .Pp There is an example style.css stylesheet file included in the distribution. .Sh FILES WRITTEN @@ -37,6 +38,12 @@ feeds. The HTML file of the menu frame which contains navigation "anchor" links (like "#feedname") to the feed names in items.html. .El +.Sh ENVIRONMENT VARIABLES +.Bl -tag -width Ds +.It Ev SFEED_NEW_AGE +Overwrite the maximum age in seconds to mark feeds as new. +By default this is 86400, which equals one day. +.El .Sh EXIT STATUS .Ex -std .Sh EXAMPLES diff --git a/sfeed_frames.c b/sfeed_frames.c index 89838cc..53f44a6 100644 --- a/sfeed_frames.c +++ b/sfeed_frames.c @@ -1,5 +1,3 @@ -#include <sys/types.h> - #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -34,7 +32,8 @@ printfeed(FILE *fpitems, FILE *fpin, struct feed *f) } fputs("<pre>\n", fpitems); - while ((linelen = getline(&line, &linesize, fpin)) > 0) { + while ((linelen = getline(&line, &linesize, fpin)) > 0 && + !ferror(fpitems)) { if (line[linelen - 1] == '\n') line[--linelen] = '\0'; parseline(line, fields); @@ -86,10 +85,8 @@ main(int argc, char *argv[]) if (!(feeds = calloc(argc, sizeof(struct feed)))) err(1, "calloc"); - if ((comparetime = time(NULL)) == -1) - err(1, "time"); - /* 1 day is old news */ - comparetime -= 86400; + if ((comparetime = getcomparetime()) == (time_t)-1) + errx(1, "getcomparetime"); /* write main index page */ if (!(fpindex = fopen("index.html", "wb"))) @@ -114,6 +111,7 @@ main(int argc, char *argv[]) if (argc == 1) { feeds[0].name = ""; printfeed(fpitems, stdin, &feeds[0]); + checkfileerror(stdin, "<stdin>", 'r'); } else { for (i = 1; i < argc; i++) { name = ((name = strrchr(argv[i], '/'))) ? name + 1 : argv[i]; @@ -122,8 +120,8 @@ main(int argc, char *argv[]) if (!(fp = fopen(argv[i], "r"))) err(1, "fopen: %s", argv[i]); printfeed(fpitems, fp, &feeds[i - 1]); - if (ferror(fp)) - err(1, "ferror: %s", argv[i]); + checkfileerror(fp, argv[i], 'r'); + checkfileerror(fpitems, "items.html", 'w'); fclose(fp); } } @@ -174,10 +172,15 @@ main(int argc, char *argv[]) "</frameset>\n" "</html>\n", fpindex); + checkfileerror(fpindex, "index.html", 'w'); + checkfileerror(fpitems, "items.html", 'w'); + fclose(fpindex); fclose(fpitems); - if (fpmenu) + if (fpmenu) { + checkfileerror(fpmenu, "menu.html", 'w'); fclose(fpmenu); + } return 0; } diff --git a/sfeed_gopher.1 b/sfeed_gopher.1 index a5b4e3a..bc45121 100644 --- 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,8 @@ 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". +This value might be overridden through environment variables. +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 @@ -45,12 +46,15 @@ The default is "127.0.0.1". .It Ev SFEED_GOPHER_PORT This environment variable can be used as the Gopher Port field. The default is "70". +.It Ev SFEED_NEW_AGE +Overwrite the maximum age in seconds to mark feeds as new. +By default this is 86400, which equals one day. .El .Sh EXIT STATUS .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 index 9e8e10a..3512b72 100644 --- a/sfeed_gopher.c +++ b/sfeed_gopher.c @@ -1,6 +1,3 @@ -#include <sys/types.h> - -#include <limits.h> #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -8,14 +5,13 @@ #include "util.h" -static struct feed f; static char *prefixpath = "/", *host = "127.0.0.1", *port = "70"; /* default */ static char *line; static size_t linesize; static time_t comparetime; /* Escape characters in gopher, CR and LF are ignored */ -void +static void gophertext(FILE *fp, const char *s) { for (; *s; s++) { @@ -50,7 +46,8 @@ printfeed(FILE *fpitems, FILE *fpin, struct feed *f) fprintf(fpitems, "i\t\t%s\t%s\r\n", host, port); } - while ((linelen = getline(&line, &linesize, fpin)) > 0) { + while ((linelen = getline(&line, &linesize, fpin)) > 0 && + !ferror(fpitems)) { if (line[linelen - 1] == '\n') line[--linelen] = '\0'; parseline(line, fields); @@ -64,7 +61,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 is a gopher URL then change it into a DirEntity */ if (!strncmp(fields[FieldLink], "gopher://", 9) && uri_parse(fields[FieldLink], &u) != -1) { itemhost = u.host; @@ -121,9 +118,10 @@ printfeed(FILE *fpitems, FILE *fpin, struct feed *f) int main(int argc, char *argv[]) { + struct feed f = { 0 }; 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) @@ -137,10 +135,8 @@ main(int argc, char *argv[]) err(1, "pledge"); } - if ((comparetime = time(NULL)) == -1) - err(1, "time"); - /* 1 day is old news */ - comparetime -= 86400; + if ((comparetime = getcomparetime()) == (time_t)-1) + errx(1, "getcomparetime"); if ((p = getenv("SFEED_GOPHER_HOST"))) host = p; @@ -150,6 +146,8 @@ main(int argc, char *argv[]) if (argc == 1) { f.name = ""; printfeed(stdout, stdin, &f); + checkfileerror(stdin, "<stdin>", 'r'); + checkfileerror(stdout, "<stdout>", 'w'); } else { if ((p = getenv("SFEED_GOPHER_PATH"))) prefixpath = p; @@ -165,15 +163,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); - if (ferror(fp)) - err(1, "ferror: %s", argv[i]); + checkfileerror(fp, argv[i], 'r'); + checkfileerror(fpitems, name, 'w'); fclose(fp); fclose(fpitems); @@ -182,10 +176,11 @@ 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); + checkfileerror(fpindex, "index", 'w'); fclose(fpindex); } diff --git a/sfeed_html.1 b/sfeed_html.1 index efeb289..e517a63 100644 --- a/sfeed_html.1 +++ b/sfeed_html.1 @@ -26,9 +26,16 @@ is empty. .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. +This value might be overridden through environment variables. Items are marked as new using a bold markup. .Pp There is an example style.css stylesheet file included in the distribution. +.Sh ENVIRONMENT VARIABLES +.Bl -tag -width Ds +.It Ev SFEED_NEW_AGE +Overwrite the maximum age in seconds to mark feeds as new. +By default this is 86400, which equals one day. +.El .Sh EXIT STATUS .Ex -std .Sh EXAMPLES diff --git a/sfeed_html.c b/sfeed_html.c index d8abda7..2142145 100644 --- a/sfeed_html.c +++ b/sfeed_html.c @@ -1,5 +1,3 @@ -#include <sys/types.h> - #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -34,7 +32,8 @@ printfeed(FILE *fp, struct feed *f) } fputs("<pre>\n", stdout); - while ((linelen = getline(&line, &linesize, fp)) > 0) { + while ((linelen = getline(&line, &linesize, fp)) > 0 && + !ferror(stdout)) { if (line[linelen - 1] == '\n') line[--linelen] = '\0'; parseline(line, fields); @@ -86,10 +85,8 @@ main(int argc, char *argv[]) if (!(feeds = calloc(argc, sizeof(struct feed)))) err(1, "calloc"); - if ((comparetime = time(NULL)) == -1) - err(1, "time"); - /* 1 day is old news */ - comparetime -= 86400; + if ((comparetime = getcomparetime()) == (time_t)-1) + errx(1, "getcomparetime"); fputs("<!DOCTYPE HTML>\n" "<html>\n" @@ -109,8 +106,7 @@ main(int argc, char *argv[]) if (argc == 1) { feeds[0].name = ""; printfeed(stdin, &feeds[0]); - if (ferror(stdin)) - err(1, "ferror: <stdin>:"); + checkfileerror(stdin, "<stdin>", 'r'); } else { for (i = 1; i < argc; i++) { name = ((name = strrchr(argv[i], '/'))) ? name + 1 : argv[i]; @@ -118,8 +114,8 @@ main(int argc, char *argv[]) if (!(fp = fopen(argv[i], "r"))) err(1, "fopen: %s", argv[i]); printfeed(fp, &feeds[i - 1]); - if (ferror(fp)) - err(1, "ferror: %s", argv[i]); + checkfileerror(fp, argv[i], 'r'); + checkfileerror(stdout, "<stdout>", 'w'); fclose(fp); } } @@ -150,5 +146,7 @@ main(int argc, char *argv[]) fprintf(stdout, "\t</body>\n\t<title>(%lu/%lu) - Newsfeed</title>\n</html>\n", totalnew, total); + checkfileerror(stdout, "<stdout>", 'w'); + return 0; } diff --git a/sfeed_json.1 b/sfeed_json.1 new file mode 100644 index 0000000..4331bd7 --- /dev/null +++ b/sfeed_json.1 @@ -0,0 +1,49 @@ +.Dd August 1, 2023 +.Dt SFEED_JSON 1 +.Os +.Sh NAME +.Nm sfeed_json +.Nd format feed data to JSON Feed +.Sh SYNOPSIS +.Nm +.Op Ar +.Sh DESCRIPTION +.Nm +formats feed data (TSV) from +.Xr sfeed 1 +from stdin or for each +.Ar file +to stdout as JSON Feed data. +If one or more +.Ar file +arguments are specified then the basename of the +.Ar file +is used as the feed name in the output. +If no +.Ar file +arguments are specified and so the data is read from stdin then the feed name +is empty. +.Pp +If +.Nm +is reading from one or more +.Ar file +arguments it will prefix the entry title with "[feed name] ". +.Sh EXIT STATUS +.Ex -std +.Sh EXAMPLES +.Bd -literal +curl -s 'https://codemadness.org/atom.xml' | sfeed | sfeed_json +.Ed +.Sh SEE ALSO +.Xr sfeed 1 , +.Xr sfeed_atom 1 , +.Xr sfeed 5 +.Sh STANDARDS +.Rs +.%T JSON Feed Version 1.1 +.%U https://www.jsonfeed.org/version/1.1/ +.%D Nov, 2022 +.Re +.Sh AUTHORS +.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org diff --git a/sfeed_json.c b/sfeed_json.c new file mode 100644 index 0000000..4e1f52c --- /dev/null +++ b/sfeed_json.c @@ -0,0 +1,172 @@ +#include <stdio.h> +#include <string.h> +#include <time.h> + +#include "util.h" + +static char *line; +static size_t linesize; +static int firstitem = 1; + +/* Unescape / decode fields printed by string_print_encoded() */ +static void +printcontent(const char *s) +{ + for (; *s; s++) { + switch (*s) { + case '\\': + if (*(s + 1) == '\0') + break; + s++; + switch (*s) { + case 'n': fputs("\\n", stdout); break; + case '\\': fputs("\\\\", stdout); break; + case 't': fputs("\\t", stdout); break; + } + break; /* ignore invalid escape sequence */ + case '"': fputs("\\\"", stdout); break; + default: + putchar(*s); + break; + } + } +} + +static void +printfield(const char *s) +{ + for (; *s; s++) { + if (*s == '\\') + fputs("\\\\", stdout); + else if (*s == '"') + fputs("\\\"", stdout); + else + putchar(*s); + } +} + +static void +printfeed(FILE *fp, const char *feedname) +{ + char *fields[FieldLast], timebuf[32]; + struct tm parsedtm, *tm; + time_t parsedtime; + ssize_t linelen; + int ch; + char *p, *s; + + while ((linelen = getline(&line, &linesize, fp)) > 0 && + !ferror(stdout)) { + if (line[linelen - 1] == '\n') + line[--linelen] = '\0'; + parseline(line, fields); + + if (!firstitem) + fputs(",\n", stdout); + firstitem = 0; + + fputs("{\n\t\"id\": \"", stdout); + printfield(fields[FieldId]); + fputs("\"", stdout); + + parsedtime = 0; + if (!strtotime(fields[FieldUnixTimestamp], &parsedtime) && + (tm = gmtime_r(&parsedtime, &parsedtm)) && + strftime(timebuf, sizeof(timebuf), "%Y-%m-%dT%H:%M:%SZ", tm)) { + fputs(",\n\t\"date_published\": \"", stdout); + fputs(timebuf, stdout); + fputs("\"", stdout); + } + + fputs(",\n\t\"title\": \"", stdout); + if (feedname[0]) { + fputs("[", stdout); + printfield(feedname); + fputs("] ", stdout); + } + printfield(fields[FieldTitle]); + fputs("\"", stdout); + + if (fields[FieldLink][0]) { + fputs(",\n\t\"url\": \"", stdout); + printfield(fields[FieldLink]); + fputs("\"", stdout); + } + + if (fields[FieldAuthor][0]) { + fputs(",\n\t\"authors\": [{\"name\": \"", stdout); + printfield(fields[FieldAuthor]); + fputs("\"}]", stdout); + } + + if (fields[FieldCategory][0]) { + fputs(",\n\t\"tags\": [", stdout); + + for (p = s = fields[FieldCategory]; ; s++) { + if (*s == '|' || *s == '\0') { + if (p != fields[FieldCategory]) + fputs(", ", stdout); + ch = *s; + *s = '\0'; /* temporary NUL terminate */ + fputs("\"", stdout); + printfield(p); + fputs("\"", stdout); + *s = ch; /* restore */ + p = s + 1; + } + if (*s == '\0') + break; + } + fputs("]", stdout); + } + + if (fields[FieldEnclosure][0]) { + fputs(",\n\t\"attachments\": [{\"url\": \"", stdout); + printfield(fields[FieldEnclosure]); + fputs("\"}]", stdout); + } + + if (!strcmp(fields[FieldContentType], "html")) + fputs(",\n\t\"content_html\": \"", stdout); + else + fputs(",\n\t\"content_text\": \"", stdout); + printcontent(fields[FieldContent]); + fputs("\"\n}", stdout); + } +} + +int +main(int argc, char *argv[]) +{ + FILE *fp; + char *name; + int i; + + if (pledge(argc == 1 ? "stdio" : "stdio rpath", NULL) == -1) + err(1, "pledge"); + + fputs("{\n" + "\"version\": \"https://jsonfeed.org/version/1.1\",\n" + "\"title\": \"Newsfeed\",\n" + "\"items\": [\n", stdout); + + if (argc == 1) { + printfeed(stdin, ""); + checkfileerror(stdin, "<stdin>", 'r'); + } else { + for (i = 1; i < argc; i++) { + if (!(fp = fopen(argv[i], "r"))) + err(1, "fopen: %s", argv[i]); + name = ((name = strrchr(argv[i], '/'))) ? name + 1 : argv[i]; + printfeed(fp, name); + checkfileerror(fp, argv[i], 'r'); + checkfileerror(stdout, "<stdout>", 'w'); + fclose(fp); + } + } + fputs("]\n}\n", stdout); + + checkfileerror(stdout, "<stdout>", 'w'); + + return 0; +} diff --git a/sfeed_markread b/sfeed_markread index a40e572..cad71bc 100755 --- 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 @@ -18,9 +18,9 @@ read) cat >> "${urlfile}" ;; unread) - tmp=$(mktemp) + tmp="$(mktemp)" || exit 1 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 index cd6e65d..c00971f 100644 --- a/sfeed_mbox.c +++ b/sfeed_mbox.c @@ -22,33 +22,35 @@ 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) { escapefrom: for (; *s == '>'; s++) - fputc('>', fp); + putc('>', fp); /* escape "From ", mboxrd-style. */ if (!strncmp(s, "From ", 5)) - fputc('>', fp); + putc('>', fp); for (; *s; s++) { switch (*s) { case '\\': + if (*(s + 1) == '\0') + break; s++; switch (*s) { case 'n': - fputc('\n', fp); + putc('\n', fp); s++; goto escapefrom; - case '\\': fputc('\\', fp); break; - case 't': fputc('\t', fp); break; + case '\\': putc('\\', fp); break; + case 't': putc('\t', fp); break; } break; default: - fputc(*s, fp); break; + putc(*s, fp); break; } } } @@ -63,7 +65,8 @@ printfeed(FILE *fp, const char *feedname) ssize_t linelen; int ishtml; - while ((linelen = getline(&line, &linesize, fp)) > 0) { + while ((linelen = getline(&line, &linesize, fp)) > 0 && + !ferror(stdout)) { if (line[linelen - 1] == '\n') line[--linelen] = '\0'; hash = djb2((unsigned char *)line, 5381ULL); @@ -81,7 +84,7 @@ printfeed(FILE *fp, const char *feedname) printf("Date: %s\n", dtimebuf); /* invalid/missing: use current time */ } - printf("From: %s <sfeed@>\n", fields[FieldAuthor][0] ? fields[FieldAuthor] : feedname); + printf("From: %s <anonymous@>\n", fields[FieldAuthor][0] ? fields[FieldAuthor] : feedname); printf("To: %s <%s@%s>\n", user, user, host); printf("Subject: %s\n", fields[FieldTitle]); printf("Message-ID: <%s%s%llu@%s>\n", @@ -152,8 +155,8 @@ main(int argc, char *argv[]) user = "you"; if (gethostname(host, sizeof(host)) == -1) err(1, "gethostname"); - if ((now = time(NULL)) == -1) - err(1, "time"); + if ((now = time(NULL)) == (time_t)-1) + errx(1, "time"); if (!gmtime_r(&now, &tmnow)) err(1, "gmtime_r: can't get current time"); if (!strftime(mtimebuf, sizeof(mtimebuf), "%a %b %d %H:%M:%S %Y", &tmnow)) @@ -163,17 +166,20 @@ main(int argc, char *argv[]) if (argc == 1) { printfeed(stdin, ""); + checkfileerror(stdin, "<stdin>", 'r'); } else { for (i = 1; i < argc; i++) { if (!(fp = fopen(argv[i], "r"))) err(1, "fopen: %s", argv[i]); name = ((name = strrchr(argv[i], '/'))) ? name + 1 : argv[i]; printfeed(fp, name); - if (ferror(fp)) - err(1, "ferror: %s", argv[i]); + checkfileerror(fp, argv[i], 'r'); + checkfileerror(stdout, "<stdout>", 'w'); fclose(fp); } } + checkfileerror(stdout, "<stdout>", 'w'); + return 0; } diff --git a/sfeed_opml_export b/sfeed_opml_export index eb22520..f949488 100755 --- a/sfeed_opml_export +++ b/sfeed_opml_export @@ -7,19 +7,19 @@ loadconfig() { if [ "$1" != "" ]; then # get absolute path of config file required for including. config="$1" - path=$(readlink -f "${config}" 2>/dev/null) + configpath=$(readlink -f "${config}" 2>/dev/null) else # default config location. config="$HOME/.sfeed/sfeedrc" - path="${config}" + configpath="${config}" fi # config is loaded here to be able to override $sfeedpath or functions. - if [ -r "${path}" ]; then - . "${path}" + if [ -r "${configpath}" ] && [ -f "${configpath}" ]; then + . "${configpath}" 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. @@ -38,12 +38,16 @@ cat <<! <?xml version="1.0" encoding="UTF-8"?> <opml version="1.0"> <head> - <title>OPML export from sfeed</title> + <title>OPML export</title> </head> <body> ! -feeds | awk -F '\t' '{ +feeds | LC_ALL=C awk ' +BEGIN { + FS = "\x1f"; RS = "\x1e"; +} +{ gsub("&", "\\&"); gsub("\"", "\\""); gsub("'"'"'", "\\'"); diff --git a/sfeed_opml_import.c b/sfeed_opml_import.c index 0844b5c..ce33aac 100644 --- 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); @@ -71,7 +70,7 @@ static void xmlattrentity(XMLParser *p, const char *t, size_t tl, const char *n, size_t nl, const char *v, size_t vl) { - char buf[16]; + char buf[8]; int len; if ((len = xml_entitytostr(v, buf, sizeof(buf))) > 0) @@ -97,9 +96,12 @@ main(void) "# list of feeds to fetch:\n" "feeds() {\n" " # feed <name> <feedurl> [basesiteurl] [encoding]\n", stdout); - /* NOTE: getnext is defined in xml.h for inline optimization */ + /* NOTE: GETNEXT is defined in xml.h for inline optimization */ xml_parse(&parser); fputs("}\n", stdout); + checkfileerror(stdin, "<stdin>", 'r'); + checkfileerror(stdout, "<stdout>", 'w'); + return 0; } diff --git a/sfeed_plain.1 b/sfeed_plain.1 index 559ce77..2e8e6ad 100644 --- 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,8 @@ 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". +This value might be overridden through environment variables. +Items are marked as new with the prefix "N" at the start of the line. .Pp .Nm aligns the output. @@ -39,6 +40,12 @@ per rune, using .Xr mbtowc 3 and .Xr wcwidth 3 . +.Sh ENVIRONMENT VARIABLES +.Bl -tag -width Ds +.It Ev SFEED_NEW_AGE +Overwrite the maximum age in seconds to mark feeds as new. +By default this is 86400, which equals one day. +.El .Sh EXIT STATUS .Ex -std .Sh EXAMPLES diff --git a/sfeed_plain.c b/sfeed_plain.c index df93a5a..adeefdb 100644 --- a/sfeed_plain.c +++ b/sfeed_plain.c @@ -1,5 +1,3 @@ -#include <sys/types.h> - #include <locale.h> #include <stdio.h> #include <string.h> @@ -19,7 +17,8 @@ printfeed(FILE *fp, const char *feedname) time_t parsedtime; ssize_t linelen; - while ((linelen = getline(&line, &linesize, fp)) > 0) { + while ((linelen = getline(&line, &linesize, fp)) > 0 && + !ferror(stdout)) { if (line[linelen - 1] == '\n') line[--linelen] = '\0'; parseline(line, fields); @@ -62,24 +61,24 @@ main(int argc, char *argv[]) if (pledge(argc == 1 ? "stdio" : "stdio rpath", NULL) == -1) err(1, "pledge"); - if ((comparetime = time(NULL)) == -1) - err(1, "time"); - /* 1 day is old news */ - comparetime -= 86400; + if ((comparetime = getcomparetime()) == (time_t)-1) + errx(1, "getcomparetime"); if (argc == 1) { printfeed(stdin, ""); + checkfileerror(stdin, "<stdin>", 'r'); } else { for (i = 1; i < argc; i++) { if (!(fp = fopen(argv[i], "r"))) err(1, "fopen: %s", argv[i]); name = ((name = strrchr(argv[i], '/'))) ? name + 1 : argv[i]; printfeed(fp, name); - if (ferror(fp)) - err(1, "ferror: %s", argv[i]); + checkfileerror(fp, argv[i], 'r'); + checkfileerror(stdout, "<stdout>", 'w'); fclose(fp); } } + checkfileerror(stdout, "<stdout>", 'w'); return 0; } diff --git a/sfeed_twtxt.c b/sfeed_twtxt.c index 6107fc7..1bb9d3b 100644 --- a/sfeed_twtxt.c +++ b/sfeed_twtxt.c @@ -1,5 +1,3 @@ -#include <sys/types.h> - #include <stdio.h> #include <string.h> #include <time.h> @@ -17,7 +15,8 @@ printfeed(FILE *fp, const char *feedname) time_t parsedtime; ssize_t linelen; - while ((linelen = getline(&line, &linesize, fp)) > 0) { + while ((linelen = getline(&line, &linesize, fp)) > 0 && + !ferror(stdout)) { if (line[linelen - 1] == '\n') line[--linelen] = '\0'; parseline(line, fields); @@ -54,17 +53,20 @@ main(int argc, char *argv[]) if (argc == 1) { printfeed(stdin, ""); + checkfileerror(stdin, "<stdin>", 'r'); } else { for (i = 1; i < argc; i++) { if (!(fp = fopen(argv[i], "r"))) err(1, "fopen: %s", argv[i]); name = ((name = strrchr(argv[i], '/'))) ? name + 1 : argv[i]; printfeed(fp, name); - if (ferror(fp)) - err(1, "ferror: %s", argv[i]); + checkfileerror(fp, argv[i], 'r'); + checkfileerror(stdout, "<stdout>", 'w'); fclose(fp); } } + checkfileerror(stdout, "<stdout>", 'w'); + return 0; } diff --git a/sfeed_update b/sfeed_update index ba9e242..fd468a5 100755 --- a/sfeed_update +++ b/sfeed_update @@ -7,7 +7,7 @@ sfeedpath="$HOME/.sfeed/feeds" # used for processing feeds concurrently: wait until ${maxjobs} amount of # feeds are finished at a time. -maxjobs=8 +maxjobs=16 # load config (evaluate shellscript). # loadconfig(configfile) @@ -16,26 +16,33 @@ loadconfig() { if [ "$1" != "" ]; then # get absolute path of config file required for including. config="$1" - path=$(readlink -f "${config}" 2>/dev/null) + configpath=$(readlink -f "${config}" 2>/dev/null) else # default config location. config="$HOME/.sfeed/sfeedrc" - path="${config}" + configpath="${config}" fi # config is loaded here to be able to override $sfeedpath or functions. - if [ -r "${path}" ]; then - . "${path}" + if [ -r "${configpath}" ] && [ -f "${configpath}" ]; then + . "${configpath}" else - echo "Configuration file \"${config}\" cannot be read." >&2 - echo "See sfeedrc.example for an example." >&2 - exit 1 + 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 + die fi } # log(name, s) log() { + printf '[%s] %-50.50s %s\n' "$(date +'%H:%M:%S')" "$1" "$2" +} + +# log_error(name, s) +log_error() { printf '[%s] %-50.50s %s\n' "$(date +'%H:%M:%S')" "$1" "$2" >&2 + # set error exit status indicator for parallel jobs. + rm -f "${sfeedtmpdir}/ok" } # fetch a feed via HTTP/HTTPS etc. @@ -64,7 +71,7 @@ parse() { } # filter fields. -# filter(name) +# filter(name, url) filter() { cat } @@ -76,9 +83,9 @@ merge() { } # order by timestamp (descending). -# order(name) +# order(name, url) order() { - sort -t ' ' -k1rn,1 + sort -t ' ' -k1rn,1 2>/dev/null } # internal handler to fetch and process a feed. @@ -91,101 +98,111 @@ _feed() { filename="$(printf '%s' "${name}" | tr '/' '_')" sfeedfile="${sfeedpath}/${filename}" - tmpfeedfile="${sfeedtmpdir}/${filename}" + tmpfeedfile="${sfeedtmpdir}/feeds/${filename}" # if file does not exist yet create it. [ -e "${sfeedfile}" ] || touch "${sfeedfile}" 2>/dev/null if ! fetch "${name}" "${feedurl}" "${sfeedfile}" > "${tmpfeedfile}.fetch"; then - log "${name}" "FAIL (FETCH)" - return + log_error "${name}" "FAIL (FETCH)" + return 1 fi # try to detect encoding (if not specified). if detecting the encoding fails assume utf-8. [ "${encoding}" = "" ] && encoding=$(sfeed_xmlenc < "${tmpfeedfile}.fetch") if ! convertencoding "${name}" "${encoding}" "utf-8" < "${tmpfeedfile}.fetch" > "${tmpfeedfile}.utf8"; then - log "${name}" "FAIL (ENCODING)" - return + log_error "${name}" "FAIL (ENCODING)" + return 1 fi rm -f "${tmpfeedfile}.fetch" # if baseurl is empty then use feedurl. if ! parse "${name}" "${feedurl}" "${basesiteurl:-${feedurl}}" < "${tmpfeedfile}.utf8" > "${tmpfeedfile}.tsv"; then - log "${name}" "FAIL (PARSE)" - return + log_error "${name}" "FAIL (PARSE)" + return 1 fi rm -f "${tmpfeedfile}.utf8" - if ! filter "${name}" < "${tmpfeedfile}.tsv" > "${tmpfeedfile}.filter"; then - log "${name}" "FAIL (FILTER)" - return + if ! filter "${name}" "${feedurl}" < "${tmpfeedfile}.tsv" > "${tmpfeedfile}.filter"; then + log_error "${name}" "FAIL (FILTER)" + return 1 fi rm -f "${tmpfeedfile}.tsv" # new feed data is empty: no need for below stages. if [ ! -s "${tmpfeedfile}.filter" ]; then log "${name}" "OK" - return + return 0 fi if ! merge "${name}" "${sfeedfile}" "${tmpfeedfile}.filter" > "${tmpfeedfile}.merge"; then - log "${name}" "FAIL (MERGE)" - return + log_error "${name}" "FAIL (MERGE)" + return 1 fi rm -f "${tmpfeedfile}.filter" - if ! order "${name}" < "${tmpfeedfile}.merge" > "${tmpfeedfile}.order"; then - log "${name}" "FAIL (ORDER)" - return + if ! order "${name}" "${feedurl}" < "${tmpfeedfile}.merge" > "${tmpfeedfile}.order"; then + log_error "${name}" "FAIL (ORDER)" + return 1 fi rm -f "${tmpfeedfile}.merge" # copy if ! cp "${tmpfeedfile}.order" "${sfeedfile}"; then - log "${name}" "FAIL (COPY)" - return + log_error "${name}" "FAIL (COPY)" + return 1 fi rm -f "${tmpfeedfile}.order" # OK log "${name}" "OK" + return 0 } # fetch and process a feed in parallel. # feed(name, feedurl, [basesiteurl], [encoding]) feed() { - # wait until ${maxjobs} are finished: will stall the queue if an item - # is slow, but it is portable. - [ ${signo} -ne 0 ] && return - [ $((curjobs % maxjobs)) -eq 0 ] && wait - [ ${signo} -ne 0 ] && return - curjobs=$((curjobs + 1)) - - _feed "$@" & + # Output job parameters for xargs. + # Specify fields as a single parameter separated by a NUL byte. + # The parameter is split into fields later by the child process, this + # allows using xargs with empty fields across many implementations. + printf '%s\037%s\037%s\037%s\037%s\037%s\0' \ + "${config}" "${sfeedtmpdir}" "$1" "$2" "$3" "$4" } +# cleanup() cleanup() { # remove temporary directory with feed files. rm -rf "${sfeedtmpdir}" } +# die(statuscode) +die() { + statuscode="${1:-1}" # default: exit 1 + # cleanup temporary files etc. + cleanup + exit "${statuscode}" +} + +# sighandler(signo) sighandler() { signo="$1" # ignore TERM signal for myself. trap -- "" TERM - # kill all running childs >:D + # kill all running children >:D kill -TERM -$$ } +# feeds() 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 + die } +# main(args...) main() { - # job counter. - curjobs=0 # signal number received for parent. signo=0 # SIGINT: signal to interrupt parent. @@ -195,18 +212,40 @@ main() { # load config file. loadconfig "$1" # fetch feeds and store in temporary directory. - sfeedtmpdir="$(mktemp -d '/tmp/sfeed_XXXXXX')" + sfeedtmpdir="$(mktemp -d "${TMPDIR:-/tmp}/sfeed_XXXXXX")" || die + mkdir -p "${sfeedtmpdir}/feeds" + touch "${sfeedtmpdir}/ok" || die # make sure path exists. mkdir -p "${sfeedpath}" - # fetch feeds specified in config file. - feeds - # wait till all feeds are fetched (concurrently). - [ ${signo} -eq 0 ] && wait - # cleanup temporary files etc. - cleanup + + # print feeds for parallel processing with xargs. + feeds > "${sfeedtmpdir}/jobs" || die + SFEED_UPDATE_CHILD="1" xargs -x -0 -P "${maxjobs}" -n 1 \ + "$(readlink -f "${argv0}")" < "${sfeedtmpdir}/jobs" + statuscode=$? + + # check error exit status indicator for parallel jobs. + [ -f "${sfeedtmpdir}/ok" ] || statuscode=1 # on signal SIGINT and SIGTERM exit with signal number + 128. - [ ${signo} -ne 0 ] && exit $((signo+128)) - return 0 + [ ${signo} -ne 0 ] && die $((signo+128)) + die ${statuscode} } +# process a single feed. +# parameters are: config, tmpdir, name, feedurl, basesiteurl, encoding +if [ "${SFEED_UPDATE_CHILD}" = "1" ]; then + IFS="" # "\037" + [ "$1" = "" ] && exit 0 # must have an argument set + printf '%s\n' "$1" | \ + while read -r _config _tmpdir _name _feedurl _basesiteurl _encoding; do + loadconfig "${_config}" + sfeedtmpdir="${_tmpdir}" + _feed "${_name}" "${_feedurl}" "${_basesiteurl}" "${_encoding}" + exit "$?" + done + exit 0 +fi + +# ...else parent mode: +argv0="$0" # store $0, in the zsh shell $0 is the name of the function. [ "${SFEED_UPDATE_INCLUDE}" = "1" ] || main "$@" diff --git a/sfeed_update.1 b/sfeed_update.1 index 81f2d7d..ed61726 100644 --- a/sfeed_update.1 +++ b/sfeed_update.1 @@ -1,4 +1,4 @@ -.Dd August 3, 2021 +.Dd August 1, 2023 .Dt SFEED_UPDATE 1 .Os .Sh NAME @@ -42,8 +42,6 @@ format containing all items per feed. The .Nm script merges new items with this file. -The feed name cannot contain the '/' character because it is a path separator, -they will be replaced with '_'. .El .Sh ENVIRONMENT VARIABLES .Bl -tag -width Ds @@ -54,18 +52,25 @@ can be sourced as a script, but it won't run the .Fn main entry-point. .El +.Sh LOGGING +When processing a feed it will log failures to stderr and non-failures to +stdout in the format: +.Bd -literal +[HH:MM:SS] feedname message +.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 index 2d77d4c..e25e91c 100644 --- 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); } @@ -104,7 +103,7 @@ static void xmlattrentity(XMLParser *p, const char *t, size_t tl, const char *a, size_t al, const char *v, size_t vl) { - char buf[16]; + char buf[8]; int len; if (!ishrefattr && !istypeattr) @@ -133,8 +132,11 @@ main(int argc, char *argv[]) parser.xmltagstart = xmltagstart; parser.xmltagstartparsed = xmltagstartparsed; - /* NOTE: getnext is defined in xml.h for inline optimization */ + /* NOTE: GETNEXT is defined in xml.h for inline optimization */ xml_parse(&parser); + checkfileerror(stdin, "<stdin>", 'r'); + checkfileerror(stdout, "<stdout>", 'w'); + return 0; } diff --git a/sfeed_xmlenc.c b/sfeed_xmlenc.c index c6a43d4..461c047 100644 --- 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)); } } @@ -53,8 +52,11 @@ main(void) parser.xmlattrend = xmlattrend; parser.xmltagstart = xmltagstart; - /* NOTE: getnext is defined in xml.h for inline optimization */ + /* NOTE: GETNEXT is defined in xml.h for inline optimization */ xml_parse(&parser); + checkfileerror(stdin, "<stdin>", 'r'); + checkfileerror(stdout, "<stdout>", 'w'); + return 0; } @@ -1,4 +1,4 @@ -.Dd August 5, 2021 +.Dd December 26, 2023 .Dt SFEEDRC 5 .Os .Sh NAME @@ -18,7 +18,7 @@ The default is can be used to change the amount of concurrent .Fn feed jobs. -The default is 8. +The default is 16. .El .Sh FUNCTIONS .Bl -tag -width Ds @@ -37,6 +37,9 @@ Name of the feed, this is also used as the filename for the TAB-separated feed file. The feed name cannot contain the '/' character because it is a path separator, they will be replaced with '_'. +Each +.Fa name +should be unique. .It Fa feedurl URL to fetch the RSS/Atom data from, usually a HTTP or HTTPS URL. .It Op Fa basesiteurl @@ -59,12 +62,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 +76,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 +92,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. @@ -100,16 +104,20 @@ URL of the feed. Base URL of the feed links. This argument allows to fix relative item links. .El -.It Fn filter "name" +.It Fn filter "name" "url" 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. +.It Fa url +URL of the feed. .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. @@ -118,13 +126,15 @@ Old file. .It Fa newfile New file. .El -.It Fn order "name" +.It Fn order "name" "url" 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. +.It Fa url +URL of the feed. .El .El .Sh EXAMPLES @@ -136,7 +146,7 @@ shown below: # list of feeds to fetch: feeds() { # feed <name> <feedurl> [basesiteurl] [encoding] - feed "codemadness" "https://www.codemadness.nl/atom.xml" + feed "codemadness" "https://www.codemadness.org/atom_content.xml" feed "explosm" "http://feeds.feedburner.com/Explosm" feed "golang github releases" "https://github.com/golang/go/releases.atom" feed "linux kernel" "https://www.kernel.org/feeds/kdist.xml" "https://www.kernel.org" @@ -159,8 +169,8 @@ file: .Bd -literal # 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 \\ + # allow for 1 redirect, set User-Agent, timeout is 15 seconds. + curl -L --max-redirs 1 -H "User-Agent: 007" -f -s -m 15 \e "$2" 2>/dev/null } .Ed diff --git a/sfeedrc.example b/sfeedrc.example index 5ead13d..af8e1c4 100644 --- a/sfeedrc.example +++ b/sfeedrc.example @@ -1,9 +1,12 @@ +# for more details see the sfeedrc(5) and sfeed_update(1) man pages +# and the README file. + #sfeedpath="$HOME/.sfeed/feeds" # list of feeds to fetch: feeds() { # feed <name> <feedurl> [basesiteurl] [encoding] - feed "codemadness" "https://www.codemadness.nl/atom.xml" + feed "codemadness" "https://www.codemadness.org/atom_content.xml" feed "explosm" "http://feeds.feedburner.com/Explosm" feed "golang github releases" "https://github.com/golang/go/releases.atom" feed "linux kernel" "https://www.kernel.org/feeds/kdist.xml" "https://www.kernel.org" @@ -57,3 +57,12 @@ body.frame { body.frame #sidebar br { display: none; } +@media (prefers-color-scheme: dark) { + body { + background-color: #000; + color: #bdbdbd; + } + a { + color: #56c8ff; + } +} diff --git a/themes/mono.h b/themes/mono.h index 586ede0..253d76e 100644 --- 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 index 180f651..d57ae9c 100644 --- 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 index b00111d..79e9dd2 100644 --- 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() @@ -1,4 +1,3 @@ -#include <ctype.h> #include <errno.h> #include <stdarg.h> #include <stdio.h> @@ -46,6 +45,16 @@ errx(int exitstatus, const char *fmt, ...) exit(exitstatus); } +/* Handle read or write errors for a FILE * stream */ +void +checkfileerror(FILE *fp, const char *name, int mode) +{ + if (mode == 'r' && ferror(fp)) + errx(1, "read error: %s", name); + else if (mode == 'w' && (fflush(fp) || ferror(fp))) + errx(1, "write error: %s", name); +} + /* strcasestr() included for portability */ char * strcasestr(const char *h, const char *n) @@ -56,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; @@ -72,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 */ @@ -99,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 */ @@ -309,6 +318,24 @@ strtotime(const char *s, time_t *t) return 0; } +time_t +getcomparetime(void) +{ + time_t now, t; + char *p; + + if ((now = time(NULL)) == (time_t)-1) + return (time_t)-1; + + if ((p = getenv("SFEED_NEW_AGE"))) { + if (strtotime(p, &t) == -1) + return (time_t)-1; + return now - t; + } + + return now - 86400; /* 1 day is old news */ +} + /* Escape characters below as HTML 2.0 / XML 1.0. */ void xmlencode(const char *s, FILE *fp) @@ -1,6 +1,5 @@ -#include <sys/types.h> - #include <stdio.h> +#include <time.h> #ifdef __OpenBSD__ #include <unistd.h> @@ -9,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 @@ -20,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 */ @@ -64,6 +70,8 @@ int uri_hasscheme(const char *); int uri_makeabs(struct uri *, struct uri *, struct uri *); int uri_parse(const char *, struct uri *); +void checkfileerror(FILE *, const char *, int); +time_t getcomparetime(void); void parseline(char *, char *[FieldLast]); void printutf8pad(FILE *, const char *, size_t, int); int strtotime(const char *, time_t *); @@ -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) @@ -315,7 +317,7 @@ xml_parse(XMLParser *x) x->taglen = 1; x->isshorttag = isend = 0; - /* treat processing instruction as shorttag, don't strip "?" prefix. */ + /* treat processing instruction as short tag, don't strip "?" prefix. */ if (c == '?') { x->isshorttag = 1; } else if (c == '/') { @@ -328,9 +330,11 @@ 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 </ */ + while (c != '>' && c != EOF) /* skip until > */ + c = GETNEXT(); if (x->xmltagend) x->xmltagend(x, x->tag, x->taglen, x->isshorttag); x->tag[0] = '\0'; @@ -339,12 +343,12 @@ 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); } - /* call tagend for shortform or processing instruction */ + /* call tagend for short tag or processing instruction */ if (x->isshorttag) { if (x->xmltagend) x->xmltagend(x, x->tag, x->taglen, x->isshorttag); @@ -1,5 +1,5 @@ -#ifndef _XML_H_ -#define _XML_H_ +#ifndef XML_H +#define XML_H #include <stdio.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 a short tag ? <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; |