pax_global_header00006660000000000000000000000064147641134370014524gustar00rootroot0000000000000052 comment=66c4192ad01bc01c65491f30f746cce00a5dab8d sfeed-2.2/000077500000000000000000000000001476411343700124555ustar00rootroot00000000000000sfeed-2.2/LICENSE000066400000000000000000000014101476411343700134560ustar00rootroot00000000000000ISC License Copyright (c) 2011-2025 Hiltjo Posthuma Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. sfeed-2.2/Makefile000066400000000000000000000104621476411343700141200ustar00rootroot00000000000000.POSIX: NAME = sfeed VERSION = 2.2 # curses theme, see themes/ directory. SFEED_THEME = mono # paths PREFIX = /usr/local MANPREFIX = ${PREFIX}/man DOCPREFIX = ${PREFIX}/share/doc/${NAME} RANLIB = ranlib # use system flags. SFEED_CFLAGS = ${CFLAGS} SFEED_LDFLAGS = ${LDFLAGS} SFEED_CPPFLAGS = -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=700 -D_BSD_SOURCE # uncomment for conservative locked I/O. #SFEED_CPPFLAGS = -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=700 -D_BSD_SOURCE \ # -DGETNEXT=getchar # set $SFEED_CURSES to empty to not build sfeed_curses. 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_CURSES_LDFLAGS = ${LDFLAGS} -lcurses # Linux: some distros use ncurses and require -lncurses. #SFEED_CURSES_LDFLAGS = ${LDFLAGS} -lncurses # Gentoo Linux: some distros might also require -ltinfo and -D_DEFAULT_SOURCE # to prevent warnings about feature test macros. #SFEED_CURSES_LDFLAGS = ${LDFLAGS} -lncurses -ltinfo # FreeBSD: unset feature test macros for SIGWINCH etc. #SFEED_CURSES_CPPFLAGS = # use minicurses with hardcoded escape sequences (not the system curses). #SFEED_CURSES_CPPFLAGS = -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=700 -D_BSD_SOURCE \ # -DSFEED_THEME=\"themes/${SFEED_THEME}.h\" -DSFEED_MINICURSES #SFEED_CURSES_LDFLAGS = ${LDFLAGS} BIN = \ sfeed\ sfeed_atom\ ${SFEED_CURSES}\ sfeed_frames\ sfeed_gopher\ sfeed_html\ sfeed_json\ sfeed_mbox\ sfeed_opml_import\ sfeed_plain\ sfeed_twtxt\ sfeed_web\ sfeed_xmlenc SCRIPTS = \ sfeed_content\ sfeed_markread\ sfeed_opml_export\ sfeed_update SRC = ${BIN:=.c} HDR = \ minicurses.h\ util.h\ xml.h LIBUTIL = libutil.a LIBUTILSRC = \ util.c LIBUTILOBJ = ${LIBUTILSRC:.c=.o} LIBXML = libxml.a LIBXMLSRC = \ xml.c LIBXMLOBJ = ${LIBXMLSRC:.c=.o} COMPATSRC = \ strlcat.c\ strlcpy.c COMPATOBJ =\ strlcat.o\ strlcpy.o LIB = ${LIBUTIL} ${LIBXML} ${COMPATOBJ} MAN1 = ${BIN:=.1}\ ${SCRIPTS:=.1} MAN5 = \ sfeed.5\ sfeedrc.5 DOC = \ LICENSE\ README\ README.xml all: ${BIN} ${BIN}: ${LIB} ${@:=.o} OBJ = ${SRC:.c=.o} ${LIBXMLOBJ} ${LIBUTILOBJ} ${COMPATOBJ} ${OBJ}: ${HDR} .o: ${CC} -o $@ $< ${LIB} ${SFEED_LDFLAGS} .c.o: ${CC} -o $@ -c $< ${SFEED_CFLAGS} ${SFEED_CPPFLAGS} 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 ${CC} -o $@ sfeed_curses.o ${LIB} ${SFEED_CURSES_LDFLAGS} ${LIBUTIL}: ${LIBUTILOBJ} ${AR} -rc $@ $? ${RANLIB} $@ ${LIBXML}: ${LIBXMLOBJ} ${AR} -rc $@ $? ${RANLIB} $@ dist: rm -rf "${NAME}-${VERSION}" mkdir -p "${NAME}-${VERSION}" cp -fR ${MAN1} ${MAN5} ${DOC} ${HDR} \ ${SRC} ${LIBXMLSRC} ${LIBUTILSRC} ${COMPATSRC} ${SCRIPTS} \ themes Makefile \ sfeedrc.example style.css \ "${NAME}-${VERSION}" # make tarball tar cf - "${NAME}-${VERSION}" | \ gzip -c > "${NAME}-${VERSION}.tar.gz" rm -rf "${NAME}-${VERSION}" clean: rm -f ${BIN} ${OBJ} ${LIB} install: all # installing executable files and scripts. mkdir -p "${DESTDIR}${PREFIX}/bin" cp -fRP ${BIN} ${SCRIPTS} "${DESTDIR}${PREFIX}/bin" for f in ${BIN} ${SCRIPTS}; do chmod 755 "${DESTDIR}${PREFIX}/bin/$$f"; done # installing example files. mkdir -p "${DESTDIR}${DOCPREFIX}" cp -f sfeedrc.example\ style.css\ README\ README.xml\ "${DESTDIR}${DOCPREFIX}" # installing manual pages for general commands: section 1. mkdir -p "${DESTDIR}${MANPREFIX}/man1" cp -f ${MAN1} "${DESTDIR}${MANPREFIX}/man1" for m in ${MAN1}; do chmod 644 "${DESTDIR}${MANPREFIX}/man1/$$m"; done # installing manual pages for file formats: section 5. mkdir -p "${DESTDIR}${MANPREFIX}/man5" cp -f ${MAN5} "${DESTDIR}${MANPREFIX}/man5" for m in ${MAN5}; do chmod 644 "${DESTDIR}${MANPREFIX}/man5/$$m"; done uninstall: # removing executable files and scripts. for f in ${BIN} ${SCRIPTS}; do rm -f "${DESTDIR}${PREFIX}/bin/$$f"; done # removing example files. rm -f \ "${DESTDIR}${DOCPREFIX}/sfeedrc.example"\ "${DESTDIR}${DOCPREFIX}/style.css"\ "${DESTDIR}${DOCPREFIX}/README"\ "${DESTDIR}${DOCPREFIX}/README.xml" -rmdir "${DESTDIR}${DOCPREFIX}" # removing manual pages. for m in ${MAN1}; do rm -f "${DESTDIR}${MANPREFIX}/man1/$$m"; done for m in ${MAN5}; do rm -f "${DESTDIR}${MANPREFIX}/man5/$$m"; done .PHONY: all clean dist install uninstall sfeed-2.2/README000066400000000000000000001061671476411343700133500ustar00rootroot00000000000000sfeed ----- RSS and Atom parser (and some format programs). It converts RSS or Atom feeds from XML to a TAB-separated file. There are formatting programs included to convert this TAB-separated format to various other formats. There are also some programs and scripts included to import and export OPML and to fetch, filter, merge and order feed items. Build and install ----------------- $ make # make install To build sfeed without sfeed_curses set SFEED_CURSES to an empty string: $ make SFEED_CURSES="" # make SFEED_CURSES="" install To change the theme for sfeed_curses you can set SFEED_THEME. See the themes/ directory for the theme names. $ make SFEED_THEME="templeos" # make SFEED_THEME="templeos" install Usage ----- Initial setup: mkdir -p "$HOME/.sfeed/feeds" 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 its functions and behaviour can be overridden: $EDITOR "$HOME/.sfeed/sfeedrc" or you can import existing OPML subscriptions using sfeed_opml_import(1): sfeed_opml_import < file.opml > "$HOME/.sfeed/sfeedrc" an example to export from an other RSS/Atom reader called newsboat and import for sfeed_update: newsboat -e | sfeed_opml_import > "$HOME/.sfeed/sfeedrc" an example to export from an other RSS/Atom reader called rss2email (3.x+) and import for sfeed_update: r2e opmlexport | sfeed_opml_import > "$HOME/.sfeed/sfeedrc" Update feeds, this script merges the new items, see sfeed_update(1) for more information what it can do: sfeed_update Format feeds: Plain-text list: sfeed_plain $HOME/.sfeed/feeds/* > "$HOME/.sfeed/feeds.txt" HTML view (no frames), copy style.css for a default style: cp style.css "$HOME/.sfeed/style.css" sfeed_html $HOME/.sfeed/feeds/* > "$HOME/.sfeed/feeds.html" 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 like you can make a wrapper script and add it as a cronjob. Most protocols are supported because curl(1) is used by default and also proxy settings from the environment (such as the $http_proxy environment variable) are used. The sfeed(1) program itself is just a parser that parses XML data from stdin and is therefore network protocol-agnostic. It can be used with HTTP, HTTPS, Gopher, SSH, etc. See the section "Usage and examples" below and the man-pages for more information how to use sfeed(1) and the additional tools. Dependencies ------------ - C compiler (C99). - libc (recommended: C99 and POSIX >= 200809). Optional dependencies --------------------- - POSIX make(1) for the Makefile. - 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), 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/ - iconv(1) command-line utilities, 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). - a terminal (emulator) supporting UTF-8 and the used capabilities, used by sfeed_curses(1). Optional run-time dependencies for sfeed_curses ----------------------------------------------- - xclip for yanking the URL or enclosure. See $SFEED_YANKER to change it. - xdg-open, used as a plumber by default. See $SFEED_PLUMBER to change it. - awk, used by the sfeed_content and sfeed_markread script. See the ENVIRONMENT VARIABLES section in the man page to change it. - lynx, used by the sfeed_content script to convert HTML content. See the ENVIRONMENT VARIABLES section in the man page to change it. Formats supported ----------------- 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.90+. - RDF (when used with RSS). - MediaRSS extensions (media:). - Dublin Core extensions (dc:). 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. OS tested --------- - Linux, compilers: clang, gcc, chibicc, cproc, lacc, pcc, scc, tcc, libc: glibc, musl. - OpenBSD (clang, gcc). - NetBSD (with NetBSD curses). - FreeBSD - DragonFlyBSD - GNU/Hurd - Illumos (OpenIndiana). - Windows (cygwin gcc + mintty, mingw). - HaikuOS - SerenityOS - FreeDOS (djgpp, Open Watcom). - FUZIX (sdcc -mz80, with the sfeed parser program). Architectures tested -------------------- amd64, ARM, aarch64, HPPA, i386, MIPS32-BE, RISCV64, SPARC64, Z80. Files ----- sfeed - Read XML RSS or Atom feed data from stdin. Write feed data in TAB-separated format to stdout. sfeed_atom - Format feed data (TSV) to an Atom feed. sfeed_content - View item content, for use with sfeed_curses. 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. sfeed_mbox - Format feed data (TSV) to mbox. sfeed_plain - Format feed data (TSV) to a plain-text list. sfeed_twtxt - Format feed data (TSV) to a twtxt feed. sfeed_update - Update feeds and merge items. sfeed_web - Find URLs to RSS/Atom feed from a webpage. sfeed_xmlenc - Detect character-set encoding from a XML stream. sfeedrc.example - Example config file. Can be copied to $HOME/.sfeed/sfeedrc. style.css - Example stylesheet to use with sfeed_html(1) and sfeed_frames(1). Files read at runtime by sfeed_update(1) ---------------------------------------- sfeedrc - Config file. This file is evaluated as a shellscript in sfeed_update(1). At least the following functions can be overridden per feed: - fetch: to use wget(1), OpenBSD ftp(1) or an other download program. - filter: to filter on fields. - merge: to change the merge logic. - order: to change the sort order. See also the sfeedrc(5) man page documentation for more details. The feeds() function is called to process the feeds. The default feed() function is executed concurrently as a background job in your sfeedrc(5) config file to make updating faster. The variable maxjobs can be changed to limit or increase the amount of concurrent jobs (8 by default). Files written at runtime by sfeed_update(1) ------------------------------------------- feedname - TAB-separated format containing all items per feed. The sfeed_update(1) script merges new items with this file. The format is documented in sfeed(5). File format ----------- man 5 sfeed man 5 sfeedrc man 1 sfeed Usage and examples ------------------ Find RSS/Atom feed URLs from a webpage: url="https://codemadness.org"; curl -L -s "$url" | sfeed_web "$url" output example: 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 the sfeedrc.example file. To update your feeds (configfile argument is optional): sfeed_update "configfile" Format the feeds files: # Plain-text list. sfeed_plain $HOME/.sfeed/feeds/* > $HOME/.sfeed/feeds.txt # HTML view (no frames), copy style.css for a default style. sfeed_html $HOME/.sfeed/feeds/* > $HOME/.sfeed/feeds.html # HTML view with the menu as frames, copy style.css for a default style. mkdir -p somedir && cd somedir && sfeed_frames $HOME/.sfeed/feeds/* View formatted output in your browser: $BROWSER "$HOME/.sfeed/feeds.html" View formatted output in your editor: $EDITOR "$HOME/.sfeed/feeds.txt" - - - View formatted output in a curses interface. The interface has a look inspired by the mutt mail client. It has a sidebar panel for the feeds, a panel with a listing of the items and a small statusbar for the selected item/URL. Some functions like searching and scrolling are integrated in the interface itself. Just like the other format programs included in sfeed you can run it like this: sfeed_curses ~/.sfeed/feeds/* ... or by reading from stdin: sfeed_curses < ~/.sfeed/feeds/xkcd 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" sfeed_curses ~/.sfeed/feeds/* It then uses the shellscript "sfeed_markread" to process the read and unread items. - - - Example script to view feed items in a vertical list/menu in dmenu(1). It opens the selected URL in the browser set in $BROWSER: #!/bin/sh url=$(sfeed_plain "$HOME/.sfeed/feeds/"* | dmenu -l 35 -i | \ sed -n 's@^.* \([a-zA-Z]*://\)\(.*\)$@\1\2@p') test -n "${url}" && $BROWSER "${url}" dmenu can be found at: https://git.suckless.org/dmenu/ - - - Generate a sfeedrc config file from your exported list of feeds in OPML format: sfeed_opml_import < opmlfile.xml > $HOME/.sfeed/sfeedrc - - - Export an OPML file of your feeds from a sfeedrc config file (configfile argument is optional): sfeed_opml_export configfile > myfeeds.opml - - - The filter function can be overridden in your sfeedrc file. This allows filtering items per feed. It can be used to shorten URLs, filter away advertisements, strip tracking parameters and more. # filter fields. # filter(name, url) filter() { case "$1" in "tweakers") awk -F '\t' 'BEGIN { OFS = "\t"; } # skip ads. $2 ~ /^ADV:/ { next; } # shorten link. { if (match($3, /^https:\/\/tweakers\.net\/[a-z]+\/[0-9]+\//)) { $3 = substr($3, RSTART, RLENGTH); } print $0; }';; "yt BSDNow") # filter only BSD Now from channel. awk -F '\t' '$2 ~ / \| BSD Now/';; *) cat;; esac | \ # replace youtube links with embed links. sed 's@www.youtube.com/watch?v=@www.youtube.com/embed/@g' | \ awk -F '\t' 'BEGIN { OFS = "\t"; } function filterlink(s) { # protocol must start with http, https or gopher. if (match(s, /^(http|https|gopher):\/\//) == 0) { return ""; } # shorten feedburner links. if (match(s, /^(http|https):\/\/[^\/]+\/~r\/.*\/~3\/[^\/]+\//)) { s = substr($3, RSTART, RLENGTH); } # strip tracking parameters # urchin, facebook, piwik, webtrekk and generic. gsub(/\?(ad|campaign|fbclid|pk|tm|utm|wt)_([^&]+)/, "?", s); gsub(/&(ad|campaign|fbclid|pk|tm|utm|wt)_([^&]+)/, "", s); gsub(/\?&/, "?", s); gsub(/[\?&]+$/, "", s); return s } { $3 = filterlink($3); # link $8 = filterlink($8); # enclosure # try to remove tracking pixels: tags with 1px width or height. gsub("]*(width|height)[[:space:]]*=[[:space:]]*[\"'"'"' ]?1[\"'"'"' ]?[^0-9>]+[^>]*>", "", $4); print $0; }' } - - - Aggregate feeds. This filters new entries (maximum one day old) and sorts them by newest first. Prefix the feed name in the title. Convert the TSV output data to an Atom XML feed (again): #!/bin/sh cd ~/.sfeed/feeds/ || exit 1 awk -F '\t' -v "old=$(($(date +'%s') - 86400))" ' BEGIN { OFS = "\t"; } int($1) >= old { $2 = "[" FILENAME "] " $2; print $0; }' * | \ sort -k1,1rn | \ sfeed_atom - - - To have a "tail(1) -f"-like FIFO stream filtering for new unique feed items and showing them as plain-text per line similar to sfeed_plain(1): Create a FIFO: fifo="/tmp/sfeed_fifo" mkfifo "$fifo" On the reading side: # This keeps track of unique lines so might consume much memory. # It tries to reopen the $fifo after 1 second if it fails. while :; do cat "$fifo" || sleep 1; done | awk '!x[$0]++' On the writing side: feedsdir="$HOME/.sfeed/feeds/" cd "$feedsdir" || exit 1 test -p "$fifo" || exit 1 # 1 day is old news, don't write older items. awk -F '\t' -v "old=$(($(date +'%s') - 86400))" ' BEGIN { OFS = "\t"; } int($1) >= old { $2 = "[" FILENAME "] " $2; print $0; }' * | sort -k1,1n | sfeed_plain | cut -b 3- > "$fifo" cut -b is used to trim the "N " prefix of sfeed_plain(1). - - - For some podcast feed the following code can be used to filter the latest enclosure URL (probably some audio file): awk -F '\t' 'BEGIN { latest = 0; } length($8) { ts = int($1); if (ts > latest) { url = $8; latest = ts; } } END { if (length(url)) { print url; } }' ... or on a file already sorted from newest to oldest: awk -F '\t' '$8 { print $8; exit }' - - - Over time your feeds file might become quite big. You can archive items of a feed from (roughly) the last week by doing for example: awk -F '\t' -v "old=$(($(date +'%s') - 604800))" 'int($1) > old' < feed > feed.new mv feed feed.bak mv feed.new feed This could also be run weekly in a crontab to archive the feeds. Like throwing away old newspapers. It keeps the feeds list tidy and the formatted output small. - - - Convert mbox to separate maildirs per feed and filter duplicate messages using the fdm program. fdm is available at: https://github.com/nicm/fdm fdm config file (~/.sfeed/fdm.conf): set unmatched-mail keep account "sfeed" mbox "%[home]/.sfeed/mbox" $cachepath = "%[home]/.sfeed/fdm.cache" cache "${cachepath}" $maildir = "%[home]/feeds/" # Check if message is in the cache by Message-ID. match case "^Message-ID: (.*)" in headers action { tag "msgid" value "%1" } continue # If it is in the cache, stop. match matched and in-cache "${cachepath}" key "%[msgid]" action { keep } # Not in the cache, process it and add to cache. match case "^X-Feedname: (.*)" in headers action { # Store to local maildir. maildir "${maildir}%1" add-to-cache "${cachepath}" key "%[msgid]" keep } Now run: $ sfeed_mbox ~/.sfeed/feeds/* > ~/.sfeed/mbox $ fdm -f ~/.sfeed/fdm.conf fetch Now you can view feeds in mutt(1) for example. - - - Read from mbox and filter duplicate messages using the fdm program and deliver it to a SMTP server. This works similar to the rss2email program. fdm is available at: https://github.com/nicm/fdm fdm config file (~/.sfeed/fdm.conf): set unmatched-mail keep account "sfeed" mbox "%[home]/.sfeed/mbox" $cachepath = "%[home]/.sfeed/fdm.cache" cache "${cachepath}" # Check if message is in the cache by Message-ID. match case "^Message-ID: (.*)" in headers action { tag "msgid" value "%1" } continue # If it is in the cache, stop. match matched and in-cache "${cachepath}" key "%[msgid]" action { keep } # Not in the cache, process it and add to cache. match case "^X-Feedname: (.*)" in headers action { # Connect to a SMTP server and attempt to deliver the # mail to it. # Of course change the server and e-mail below. smtp server "codemadness.org" to "hiltjo@codemadness.org" add-to-cache "${cachepath}" key "%[msgid]" keep } Now run: $ sfeed_mbox ~/.sfeed/feeds/* > ~/.sfeed/mbox $ fdm -f ~/.sfeed/fdm.conf fetch Now you can view feeds in mutt(1) for example. - - - Convert mbox to separate maildirs per feed and filter duplicate messages using procmail(1). procmail_maildirs.sh file: maildir="$HOME/feeds" feedsdir="$HOME/.sfeed/feeds" procmailconfig="$HOME/.sfeed/procmailrc" # message-id cache to prevent duplicates. mkdir -p "${maildir}/.cache" if ! test -r "${procmailconfig}"; then 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 find "${feedsdir}" -type f -exec printf '%s\n' {} \; | while read -r d; do name=$(basename "${d}") mkdir -p "${maildir}/${name}/cur" mkdir -p "${maildir}/${name}/new" mkdir -p "${maildir}/${name}/tmp" printf 'Mailbox %s\n' "${name}" sfeed_mbox "${d}" | formail -s procmail "${procmailconfig}" done Procmailrc(5) file: # Example for use with sfeed_mbox(1). # The header X-Feedname is used to split into separate maildirs. It is # assumed this name is sane. MAILDIR="$HOME/feeds/" :0 * ^X-Feedname: \/.* { FEED="$MATCH" :0 Wh: "msgid_$FEED.lock" | formail -D 1024000 ".cache/msgid_$FEED.cache" :0 "$FEED"/ } Now run: $ procmail_maildirs.sh Now you can view feeds in mutt(1) for example. - - - The fetch function can be overridden in your sfeedrc file. This allows to replace the default curl(1) for sfeed_update with any other client to fetch the RSS/Atom data or change the default curl options: # fetch a feed via HTTP/HTTPS etc. # fetch(name, url, feedfile) fetch() { hurl -m 1048576 -t 15 "$2" 2>/dev/null } - - - Caching, incremental data updates and bandwidth saving For servers that support it some incremental updates and bandwidth saving can be done by using the "ETag" HTTP header. Create a directory for storing the ETags and modification timestamps per feed: mkdir -p ~/.sfeed/etags ~/.sfeed/lastmod The curl ETag options (--etag-save and --etag-compare) can be used to store and send the previous ETag header value. curl version 7.73+ is recommended for it to work properly. The curl -z option can be used to send the modification date of a local file as a HTTP "If-Modified-Since" request header. The server can then respond if the data is modified or not or respond with only the incremental data. The curl --compressed option can be used to indicate the client supports decompression. Because RSS/Atom feeds are textual XML content this generally compresses very well. These options can be set by overriding the fetch() function in the sfeedrc file: # fetch(name, url, feedfile) fetch() { basename="$(basename "$3")" etag="$HOME/.sfeed/etags/${basename}" lastmod="$HOME/.sfeed/lastmod/${basename}" output="${sfeedtmpdir}/feeds/${filename}.xml" curl \ -f -s -m 15 \ -L --max-redirs 0 \ -H "User-Agent: sfeed" \ --compressed \ --etag-save "${etag}" --etag-compare "${etag}" \ -R -o "${output}" \ -z "${lastmod}" \ "$2" 2>/dev/null || return 1 # succesful, but no file written: assume it is OK and Not Modified. [ -e "${output}" ] || return 0 # use server timestamp from curl -R to set Last-Modified. touch -r "${output}" "${lastmod}" 2>/dev/null cat "${output}" 2>/dev/null # use write output status, other errors are ignored here. fetchstatus="$?" rm -f "${output}" 2>/dev/null return "${fetchstatus}" } These options can come at a cost of some privacy, because it exposes 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 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: curl -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 Firefox/78.0' The above example string pretends to be a Windows 10 (x86-64) machine running Firefox 78. - - - Page redirects For security and efficiency reasons by default redirects are not allowed and are treated as an error. For example to prevent hijacking an unencrypted http:// to https:// redirect or to not add time of an unnecessary page redirect each time. It is encouraged to use the final redirected URL in the sfeedrc config file. If you want to ignore this advise you can override the fetch() function in the sfeedrc file and change the curl options "-L --max-redirs 0". - - - Shellscript to handle URLs and enclosures in parallel using xargs -P. This can be used to download and process URLs for downloading podcasts, webcomics, download and convert webpages, mirror videos, etc. It uses a plain-text cache file for remembering processed URLs. The match patterns are defined in the shellscript fetch() function and in the awk script and can be modified to handle items differently depending on their context. The arguments for the script are files in the sfeed(5) format. If no file 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), yt-dlp. cachefile="${SFEED_CACHEFILE:-$HOME/.sfeed/downloaded_urls}" jobs="${SFEED_JOBS:-4}" lockfile="${HOME}/.sfeed/sfeed_download.lock" # log(feedname, s, status) log() { if [ "$1" != "-" ]; then s="[$1] $2" else s="$2" fi printf '[%s]: %s: %s\n' "$(date +'%H:%M:%S')" "${s}" "$3" } # fetch(url, feedname) fetch() { case "$1" in *youtube.com*) 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";; esac } # downloader(url, title, feedname) downloader() { url="$1" title="$2" feedname="${3##*/}" msg="${title}: ${url}" # download directory. if [ "${feedname}" != "-" ]; then mkdir -p "${feedname}" if ! cd "${feedname}"; then log "${feedname}" "${msg}: ${feedname}" "DIR FAIL" >&2 return 1 fi fi log "${feedname}" "${msg}" "START" if fetch "${url}" "${feedname}"; then log "${feedname}" "${msg}" "OK" # append it safely in parallel to the cachefile on a # successful download. (flock 9 || exit 1 printf '%s\n' "${url}" >> "${cachefile}" ) 9>"${lockfile}" else 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 successful. downloader "$1" "$2" "$3" exit $? fi # ...else parent mode: tmp="$(mktemp)" || exit 1 trap "rm -f ${tmp}" EXIT [ -f "${cachefile}" ] || touch "${cachefile}" cat "${cachefile}" > "${tmp}" echo >> "${tmp}" # force it to have one line for awk. LC_ALL=C awk -F '\t' ' # fast prefilter what to download or not. function filter(url, field, feedname) { u = tolower(url); return (match(u, "youtube\\.com") || match(u, "\\.(flac|ogg|m3u|m3u8|m4a|mkv|mp3|mp4|wav|webm)$")); } function download(url, field, title, filename) { if (!length(url) || urls[url] || !filter(url, field, filename)) return; # NUL-separated for xargs -0. printf("%s%c%s%c%s%c", url, 0, title, 0, filename, 0); urls[url] = 1; # print once } { FILENR += (FNR == 1); } # lookup table from cachefile which contains downloaded URLs. FILENR == 1 { urls[$0] = 1; } # feed file(s). FILENR != 1 { download($3, 3, $2, FILENAME); # link download($8, 8, $2, FILENAME); # enclosure } ' "${tmp}" "${@:--}" | \ SFEED_DOWNLOAD_CHILD="1" xargs -r -0 -L 3 -P "${jobs}" "$(readlink -f "$0")" - - - Shellscript to export existing newsboat cached items from sqlite3 to the sfeed TSV format. #!/bin/sh # Export newsbeuter/newsboat cached items from sqlite3 to the sfeed TSV format. # The data is split per file per feed with the name of the newsboat title/url. # It writes the URLs of the read items line by line to a "urls" file. # # Dependencies: sqlite3, awk. # # Usage: create some directory to store the feeds then run this script. # newsboat cache.db file. cachefile="$HOME/.newsboat/cache.db" test -n "$1" && cachefile="$1" # dump data. # .mode ascii: Columns/rows delimited by 0x1F and 0x1E # get the first fields in the order of the sfeed(5) format. sqlite3 "$cachefile" < "/dev/stderr"; } contenttype = field($5); if (contenttype == "") contenttype = "html"; else if (index(contenttype, "/html") || index(contenttype, "/xhtml")) contenttype = "html"; else contenttype = "plain"; print $1 "\t" field($2) "\t" field($3) "\t" content($4) "\t" \ contenttype "\t" field($6) "\t" field($7) "\t" field($8) "\t" \ > fname; # write URLs of the read items to a file line by line. if ($11 == "0") { print $3 > "urls"; } }' - - - 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 have a keybind for this inside the program to perform a scripted action and then reload the feeds by sending the signal SIGHUP. In the input handling code you can then add a case: case 'M': forkexec((char *[]) { "markallread.sh", NULL }, 0); break; or case 'S': forkexec((char *[]) { "syncnews.sh", NULL }, 1); break; 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)" || exit 1 (cat ~/.sfeed/urls; cut -f 3 ~/.sfeed/feeds/*) | \ awk '!x[$0]++' > "$tmp" && mv "$tmp" ~/.sfeed/urls && pkill -SIGHUP sfeed_curses # reload feeds. Example of a `syncnews.sh` shellscript to update the feeds and reload them: #!/bin/sh 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 ----------------------------------------- To open an URL directly in the same terminal using the text-mode lynx browser: SFEED_PLUMBER=lynx SFEED_PLUMBER_INTERACTIVE=1 sfeed_curses ~/.sfeed/feeds/* Yank to tmux buffer ------------------- This changes the yank command to set the tmux buffer, instead of X11 xclip: SFEED_YANKER="tmux set-buffer \`cat\`" Alternative for xargs -P and -0 ------------------------------- Most xargs implementations support the options -P and -0. GNU or *BSD has supported them for over 20+ years! These functions in sfeed_update can be overridden in sfeedrc, if you don't want to use xargs: 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 "$@" & } runfeeds() { # job counter. curjobs=0 # fetch feeds specified in config file. feeds # wait till all feeds are fetched (concurrently). [ ${signo} -eq 0 ] && wait } Known terminal issues --------------------- Below lists some bugs or missing features in terminals that are found while testing sfeed_curses. Some of them might be fixed already upstream: - cygwin + mintty: the xterm mouse-encoding of the mouse position is broken for scrolling. - HaikuOS terminal: the xterm mouse-encoding of the mouse button number of the 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 ------- ISC, see LICENSE file. Author ------ Hiltjo Posthuma sfeed-2.2/README.xml000066400000000000000000000040421476411343700141340ustar00rootroot00000000000000XML parser ---------- A small XML parser. For the original version see: https://git.codemadness.org/xmlparser/ Dependencies ------------ - C compiler (ANSI). Features -------- - Relatively small parser. - Pretty simple API. - Pretty fast. - Portable - No dynamic memory allocation. Supports -------- - Tags in short-form (). - Tag attributes. - Short attributes without an explicitly set value (). - Comments - CDATA sections. - Helper function (xml_entitytostr) to convert XML 1.0 / HTML 2.0 named entities and numeric entities to UTF-8. - Reading XML from a fd, string buffer or implement a custom reader: see: XMLParser.getnext or GETNEXT() macro. Caveats ------- - It is not a compliant XML parser. - Performance: data is buffered even if a handler is not set: to make parsing faster change this code from xml.c. - The XML is not checked for errors so it will continue parsing XML data, this is by design. - Internally fixed-size buffers are used, callbacks like XMLParser.xmldata are called multiple times for the same tag if the data size is bigger than the internal buffer size (sizeof(XMLParser.data)). To differentiate between new calls for data you can use the xml*start and xml*end handlers. - It does not handle XML white-space rules for tag data. The raw values including white-space is passed. This is useful in some cases, like for HTML
 tags.
- 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 as well as DOCTYPE, ATTLIST etc.


Files used
----------

xml.c and xml.h


Interface / API
---------------

Should be trivial, see xml.c and xml.h and the examples below.


Examples
--------

sfeed_opml_import.c or sfeed_web.c or sfeed_xmlenc.c

See skeleton.c in the original xmlparser repository for a base program to start
quickly.


License
-------

ISC, see LICENSE file.
sfeed-2.2/minicurses.h000066400000000000000000000016641476411343700150160ustar00rootroot00000000000000#undef  OK
#define OK  (0)

const char *clr_eol = "\x1b[K";
const char *clear_screen = "\x1b[H\x1b[2J";
const char *cursor_address = "\x1b[%ld;%ldH";
const char *cursor_normal = "\x1b[?25h"; /* DECTCEM (in)Visible cursor */
const char *cursor_invisible = "\x1b[?25l"; /* DECTCEM (in)Visible cursor */
const char *eat_newline_glitch = (void *)1;
const char *enter_ca_mode = "\x1b[?1049h"; /* smcup */
const char *exit_ca_mode = "\x1b[?1049l"; /* rmcup */
const char *save_cursor = "\x1b""7";
const char *restore_cursor = "\x1b""8";
const char *exit_attribute_mode = "\x1b[0m";
const char *enter_bold_mode = "\x1b[1m";
const char *enter_dim_mode = "\x1b[2m";
const char *enter_reverse_mode = "\x1b[7m";

int
setupterm(char *term, int fildes, int *errret)
{
	return OK;
}

char *
tparm(char *s, long p1, long p2, ...)
{
	static char buf[32];

	if (s == cursor_address) {
		snprintf(buf, sizeof(buf), s, p1 + 1, p2 + 1);
		return buf;
	}

	return s;
}
sfeed-2.2/sfeed.1000066400000000000000000000056031476411343700136310ustar00rootroot00000000000000.Dd October 27, 2024
.Dt SFEED 1
.Os
.Sh NAME
.Nm sfeed
.Nd RSS and Atom parser
.Sh SYNOPSIS
.Nm
.Op Ar baseurl
.Sh DESCRIPTION
.Nm
reads RSS or Atom feed data (XML) from stdin.
It writes the feed data in a TAB-separated format to stdout.
If the
.Ar baseurl
argument is a valid absolute URL then the relative links or enclosures will be
made an absolute URL.
.Sh TAB-SEPARATED FORMAT FIELDS
The items are output per line in a TAB-separated format.
.Pp
For the fields title, id and author each whitespace character is replaced by a
SPACE character.
Control characters are removed.
.Pp
The content field can contain newlines and these are escaped.
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
The order and content of the fields are:
.Bl -tag -width 15n
.It 1. timestamp
UNIX timestamp in UTC+0, empty if missing or on a parse failure.
.It 2. title
Title text, HTML code in titles is ignored and is treated as plain-text.
.It 3. link
Link
.It 4. content
Content, can have plain-text or HTML code depending on the content-type field.
.It 5. content-type
"html" or "plain" if it has content.
.It 6. id
RSS item GUID or Atom id.
.It 7. author
Item, first author.
.It 8. enclosure
Item, first enclosure.
.It 9. category
Item, categories, multiple values are separated by the '|' character.
.El
.Sh EXIT STATUS
.Ex -std
.Sh EXAMPLES
.Bd -literal -offset 4n
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 -offset 4n
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 -offset 4n
mkdir -p ~/.sfeed/feeds
.Ed
.Pp
2. Copy the example
.Xr sfeedrc 5
configuration:
.Bd -literal -offset 4n
cp sfeedrc.example ~/.sfeed/sfeedrc
$EDITOR ~/.sfeed/sfeedrc
.Ed
.Pp
Or import existing OPML subscriptions using
.Xr sfeed_opml_import 1 :
.Bd -literal -offset 4n
sfeed_opml_import < file.opml > ~/.sfeed/sfeedrc
.Ed
.Pp
3. To update feeds and merge the new items with existing items:
.Bd -literal -offset 4n
sfeed_update
.Ed
.Pp
4. Format feeds to a plain-text list:
.Bd -literal -offset 4n
sfeed_plain ~/.sfeed/feeds/*
.Ed
.Pp
Or format feeds to a curses interface:
.Bd -literal -offset 4n
sfeed_curses ~/.sfeed/feeds/*
.Ed
.Pp
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 ,
.Xr sfeedrc 5
.Sh AUTHORS
.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org
.Sh CAVEATS
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.
sfeed-2.2/sfeed.5000066400000000000000000000024761476411343700136420ustar00rootroot00000000000000.Dd January 7, 2023
.Dt SFEED 5
.Os
.Sh NAME
.Nm sfeed
.Nd feed format
.Sh SYNOPSIS
.Nm
.Sh DESCRIPTION
.Xr sfeed 1
writes the feed data in a TAB-separated format to stdout.
.Sh TAB-SEPARATED FORMAT FIELDS
The items are output per line in a TAB-separated format.
.Pp
For the fields title, id and author each whitespace character is replaced by a
SPACE character.
Control characters are removed.
.Pp
The content field can contain newlines and these are escaped.
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
The order and content of the fields are:
.Bl -tag -width 15n
.It 1. timestamp
UNIX timestamp in UTC+0, empty if missing or on a parse failure.
.It 2. title
Title text, HTML code in titles is ignored and is treated as plain-text.
.It 3. link
Link
.It 4. content
Content, can have plain-text or HTML code depending on the content-type field.
.It 5. content-type
"html" or "plain" if it has content.
.It 6. id
RSS item GUID or Atom id.
.It 7. author
Item, first author.
.It 8. enclosure
Item, first enclosure.
.It 9. category
Item, categories, multiple values are separated by the '|' character.
.El
.Sh SEE ALSO
.Xr sfeed 1 ,
.Xr sfeed_plain 1
.Sh AUTHORS
.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org
sfeed-2.2/sfeed.c000066400000000000000000000725741476411343700137260ustar00rootroot00000000000000#include 
#include 
#include 
#include 
#include 
#include 

#include "util.h"
#include "xml.h"

#define ISINCONTENT(ctx)  ((ctx).iscontent && !((ctx).iscontenttag))
#define ISCONTENTTAG(ctx) (!((ctx).iscontent) && (ctx).iscontenttag)

/* these feed fields support multiple separated values */
#define ISFEEDFIELDMULTI(t) ((t) == FeedFieldCategory)

/* string and byte-length */
#define STRP(s)           s,sizeof(s)-1

enum FeedType {
	FeedTypeNone = 0,
	FeedTypeRSS  = 1,
	FeedTypeAtom = 2
};

enum ContentType {
	ContentTypeNone  = 0,
	ContentTypePlain = 1,
	ContentTypeHTML  = 2
};
static const char *contenttypes[] = { "", "plain", "html" };

/* String data / memory pool */
typedef struct string {
	char   *data;   /* data */
	size_t  len;    /* string length */
	size_t  bufsiz; /* allocated size */
} String;

/* NOTE: the order of these fields (content, date, author) indicate the
 *       priority to use them, from least important to high. */
enum TagId {
	TagUnknown = 0,
	/* RSS */
	RSSTagDcdate, RSSTagPubdate, /* creation date has higher priority */
	RSSTagTitle,
	RSSTagMediaDescription, RSSTagDescription, RSSTagContentEncoded,
	RSSTagGuid,
	RSSTagGuidPermalinkFalse,
	RSSTagGuidPermalinkTrue,
	/* must be defined after GUID, because it can be a link (isPermaLink) */
	RSSTagLink,
	RSSTagEnclosure,
	RSSTagAuthor, RSSTagDccreator,
	RSSTagCategory,
	/* Atom */
	/* creation date has higher priority */
	AtomTagModified, AtomTagUpdated, AtomTagIssued, AtomTagPublished,
	AtomTagTitle,
	AtomTagMediaDescription, AtomTagSummary, AtomTagContent,
	AtomTagId,
	AtomTagLink,
	AtomTagLinkAlternate,
	AtomTagLinkEnclosure,
	AtomTagAuthor, AtomTagAuthorName,
	AtomTagCategory,
	TagLast
};

typedef struct feedtag {
	char       *name; /* name of tag to match */
	size_t      len;  /* len of `name` */
	enum TagId  id;   /* unique ID */
} FeedTag;

typedef struct field {
	String     str;
	enum TagId tagid; /* tagid set previously, used for tag priority */
} FeedField;

enum {
	FeedFieldTime = 0, FeedFieldTitle, FeedFieldLink, FeedFieldContent,
	FeedFieldId, FeedFieldAuthor, FeedFieldEnclosure, FeedFieldCategory,
	FeedFieldLast
};

typedef struct feedcontext {
	String          *field;        /* current FeedItem field String */
	FeedField        fields[FeedFieldLast]; /* data for current item */
	FeedTag          tag;          /* unique current parsed tag */
	int              iscontent;    /* in content data */
	int              iscontenttag; /* in content tag */
	enum ContentType contenttype;  /* content-type for item */
	enum FeedType    feedtype;
	int              attrcount;    /* count item HTML element attributes */
} FeedContext;

static long long datetounix(long long, int, int, int, int, int);
static FeedTag * gettag(enum FeedType, const char *, size_t);
static long gettzoffset(const char *);
static int  isattr(const char *, size_t, const char *, size_t);
static int  istag(const char *, size_t, const char *, size_t);
static int  parsetime(const char *, long long *);
static void printfields(void);
static void string_append(String *, const char *, size_t);
static void string_buffer_realloc(String *, size_t);
static void string_clear(String *);
static void string_print_encoded(String *);
static void string_print_timestamp(String *);
static void string_print_trimmed(String *);
static void string_print_trimmed_multi(String *);
static void string_print_uri(String *);
static void xmlattr(XMLParser *, const char *, size_t, const char *, size_t,
                    const char *, size_t);
static void xmlattrentity(XMLParser *, const char *, size_t, const char *,
                          size_t, const char *, size_t);
static void xmlattrend(XMLParser *, const char *, size_t, const char *,
                       size_t);
static void xmlattrstart(XMLParser *, const char *, size_t, const char *,
                         size_t);
static void xmldata(XMLParser *, const char *, size_t);
static void xmldataentity(XMLParser *, const char *, size_t);
static void xmltagend(XMLParser *, const char *, size_t, int);
static void xmltagstart(XMLParser *, const char *, size_t);
static void xmltagstartparsed(XMLParser *, const char *, size_t, int);

/* map tag name to TagId type */
/* RSS, keep this in alphabetical order */
static const FeedTag rsstags[] = {
	{ STRP("author"),            RSSTagAuthor            },
	{ STRP("category"),          RSSTagCategory          },
	{ STRP("content:encoded"),   RSSTagContentEncoded    },
	{ STRP("dc:creator"),        RSSTagDccreator         },
	{ STRP("dc:date"),           RSSTagDcdate            },
	{ STRP("description"),       RSSTagDescription       },
	/* RSS: , Atom has  */
	{ STRP("enclosure"),         RSSTagEnclosure         },
	{ STRP("guid"),              RSSTagGuid              },
	{ STRP("link"),              RSSTagLink              },
	{ STRP("media:description"), RSSTagMediaDescription  },
	{ STRP("pubdate"),           RSSTagPubdate           },
	{ STRP("title"),             RSSTagTitle             }
};

/* Atom, keep this in alphabetical order */
static const FeedTag atomtags[] = {
	{ STRP("author"),            AtomTagAuthor           },
	{ STRP("category"),          AtomTagCategory         },
	{ STRP("content"),           AtomTagContent          },
	{ STRP("id"),                AtomTagId               },
	{ STRP("issued"),            AtomTagIssued           }, /* Atom 0.3 */
	/* Atom: , RSS has  */
	{ STRP("link"),              AtomTagLink             },
	{ STRP("media:description"), AtomTagMediaDescription },
	{ STRP("modified"),          AtomTagModified         }, /* Atom 0.3 */
	{ STRP("published"),         AtomTagPublished        },
	{ STRP("summary"),           AtomTagSummary          },
	{ STRP("title"),             AtomTagTitle            },
	{ STRP("updated"),           AtomTagUpdated          }
};

/* special case: nested  */
static const FeedTag atomtagauthor = { STRP("author"), AtomTagAuthor };
static const FeedTag atomtagauthorname = { STRP("name"), AtomTagAuthorName };

/* reference to no / unknown tag */
static const FeedTag notag = { STRP(""), TagUnknown };

/* map TagId type to RSS/Atom field, all tags must be defined */
static const int fieldmap[TagLast] = {
	[TagUnknown]               = -1,
	/* RSS */
	[RSSTagDcdate]             = FeedFieldTime,
	[RSSTagPubdate]            = FeedFieldTime,
	[RSSTagTitle]              = FeedFieldTitle,
	[RSSTagMediaDescription]   = FeedFieldContent,
	[RSSTagDescription]        = FeedFieldContent,
	[RSSTagContentEncoded]     = FeedFieldContent,
	[RSSTagGuid]               = -1,
	[RSSTagGuidPermalinkFalse] = FeedFieldId,
	[RSSTagGuidPermalinkTrue]  = FeedFieldId, /* special-case: both a link and an id */
	[RSSTagLink]               = FeedFieldLink,
	[RSSTagEnclosure]          = FeedFieldEnclosure,
	[RSSTagAuthor]             = FeedFieldAuthor,
	[RSSTagDccreator]          = FeedFieldAuthor,
	[RSSTagCategory]           = FeedFieldCategory,
	/* Atom */
	[AtomTagModified]          = FeedFieldTime,
	[AtomTagUpdated]           = FeedFieldTime,
	[AtomTagIssued]            = FeedFieldTime,
	[AtomTagPublished]         = FeedFieldTime,
	[AtomTagTitle]             = FeedFieldTitle,
	[AtomTagMediaDescription]  = FeedFieldContent,
	[AtomTagSummary]           = FeedFieldContent,
	[AtomTagContent]           = FeedFieldContent,
	[AtomTagId]                = FeedFieldId,
	[AtomTagLink]              = -1,
	[AtomTagLinkAlternate]     = FeedFieldLink,
	[AtomTagLinkEnclosure]     = FeedFieldEnclosure,
	[AtomTagAuthor]            = -1,
	[AtomTagAuthorName]        = FeedFieldAuthor,
	[AtomTagCategory]          = FeedFieldCategory
};

static const int FieldSeparator = '\t';
/* separator for multiple values in a field, separator should be 1 byte */
static const char FieldMultiSeparator[] = "|";
static struct uri baseuri;
static const char *baseurl;

static FeedContext ctx;
static XMLParser parser; /* XML parser state */
static String attrispermalink, attrrel, attrtype, tmpstr;

/* Unique tag(id) for parsed tag name. */
static FeedTag *
gettag(enum FeedType feedtype, const char *name, size_t namelen)
{
	FeedTag *r;
	size_t i;

	switch (feedtype) {
	case FeedTypeRSS:
		for (i = 0; i < sizeof(rsstags) / sizeof(rsstags[0]); i++) {
			r = (FeedTag *)&rsstags[i];
			if (r->len == namelen && !strcasecmp(r->name, name))
				return r;
		}
		break;
	case FeedTypeAtom:
		for (i = 0; i < sizeof(atomtags) / sizeof(atomtags[0]); i++) {
			r = (FeedTag *)&atomtags[i];
			if (r->len == namelen && !strcasecmp(r->name, name))
				return r;
		}
		break;
	default:
		break;
	}

	return NULL;
}

static char *
ltrim(const char *s)
{
	for (; ISSPACE((unsigned char)*s); s++)
		;
	return (char *)s;
}

static char *
rtrim(const char *s)
{
	const char *e;

	for (e = s + strlen(s); e > s && ISSPACE((unsigned char)*(e - 1)); e--)
		;
	return (char *)e;
}

/* Clear string only; don't free, prevents unnecessary reallocation. */
static void
string_clear(String *s)
{
	if (s->data)
		s->data[0] = '\0';
	s->len = 0;
}

static void
string_buffer_realloc(String *s, size_t newlen)
{
	size_t alloclen;

	if (newlen > SIZE_MAX / 2) {
		alloclen = SIZE_MAX;
	} else {
		for (alloclen = 64; alloclen <= newlen; alloclen *= 2)
			;
	}
	if (!(s->data = realloc(s->data, alloclen)))
		err(1, "realloc");
	s->bufsiz = alloclen;
}

/* Append data to String, s->data and data may not overlap. */
static void
string_append(String *s, const char *data, size_t len)
{
	if (!len)
		return;

	if (s->len >= SIZE_MAX - len) {
		errno = ENOMEM;
		err(1, "realloc");
	}

	/* check if allocation is necessary, never shrink the buffer. */
	if (s->len + len >= s->bufsiz)
		string_buffer_realloc(s, s->len + len + 1);
	memcpy(s->data + s->len, data, len);
	s->len += len;
	s->data[s->len] = '\0';
}

/* Print text, encode TABs, newlines and '\', remove other whitespace.
 * Remove leading and trailing whitespace. */
static void
string_print_encoded(String *s)
{
	const char *p, *e;

	if (!s->data || !s->len)
		return;

	p = ltrim(s->data);
	e = rtrim(p);

	for (; *p && p != e; p++) {
		switch (*p) {
		case '\n': putchar('\\'); putchar('n'); break;
		case '\\': putchar('\\'); putchar('\\'); break;
		case '\t': putchar('\\'); putchar('t'); break;
		default:
			/* ignore control chars */
			if (!ISCNTRL((unsigned char)*p))
				putchar(*p);
			break;
		}
	}
}

static void
printtrimmed(const char *s)
{
	char *p, *e;

	p = ltrim(s);
	e = rtrim(p);
	for (; *p && p != e; p++) {
		if (ISSPACE((unsigned char)*p))
			putchar(' '); /* any whitespace to space */
		else if (!ISCNTRL((unsigned char)*p))
			/* ignore other control chars */
			putchar(*p);
	}
}

/* Print text, replace TABs, carriage return and other whitespace with ' '.
 * Other control chars are removed. Remove leading and trailing whitespace. */
static void
string_print_trimmed(String *s)
{
	if (!s->data || !s->len)
		return;

	printtrimmed(s->data);
}

/* Print each field with trimmed whitespace, separated by '|'. */
static void
string_print_trimmed_multi(String *s)
{
	char *p, *e;
	int c;

	if (!s->data || !s->len)
		return;

	for (p = s->data; ; p = e + 1) {
		if ((e = strstr(p, FieldMultiSeparator))) {
			c = *e;
			*e = '\0';
			printtrimmed(p);
			*e = c; /* restore NUL byte to original character */
			fputs(FieldMultiSeparator, stdout);
		} else {
			printtrimmed(p);
			break;
		}
	}
}

/* Print URL, if it is a relative URL then it uses the global `baseurl`. */
static void
printuri(char *s)
{
	char link[4096], *p, *e;
	struct uri newuri, olduri;
	int c, r = -1;

	p = ltrim(s);
	e = rtrim(p);
	c = *e;
	*e = '\0';

	if (baseurl && !uri_hasscheme(p) &&
	    uri_parse(p, &olduri) != -1 && !olduri.proto[0] &&
	    uri_makeabs(&newuri, &olduri, &baseuri) != -1 && newuri.proto[0])
		r = uri_format(link, sizeof(link), &newuri);

	if (r >= 0 && (size_t)r < sizeof(link))
		printtrimmed(link);
	else
		printtrimmed(p);

	*e = c; /* restore NUL byte to original character */
}

/* Print URL, if it is a relative URL then it uses the global `baseurl`. */
static void
string_print_uri(String *s)
{
	if (!s->data || !s->len)
		return;

	printuri(s->data);
}

/* Print as UNIX timestamp, print nothing if the time is empty or invalid. */
static void
string_print_timestamp(String *s)
{
	long long t;

	if (!s->data || !s->len)
		return;

	if (parsetime(s->data, &t) != -1)
		printf("%lld", t);
}

/* 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)
{
	/* 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--;
			is_leap = 1;
		} else {
			is_leap = 0;
		}
		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) {
			cycles--;
			rem += 400;
		}
		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) {
				leaps = rem / 4U;
				rem %= 4U;
				is_leap = !rem;
			}
		}
		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)
		t += 86400;
	t += 86400LL * (day - 1);
	t += 3600LL * hour;
	t += 60LL * min;
	t += sec;

	return t;
}

/* Get timezone from string, return time offset in seconds from UTC.
 * NOTE: only parses timezones in RFC 822, many other timezone names are
 * ambiguous anyway.
 * 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 const struct {
		char *name;
		int offhour;
	} tzones[] = {
		{ "CDT", -5 * 3600 },
		{ "CST", -6 * 3600 },
		{ "EDT", -4 * 3600 },
		{ "EST", -5 * 3600 },
		{ "MDT", -6 * 3600 },
		{ "MST", -7 * 3600 },
		{ "PDT", -7 * 3600 },
		{ "PST", -8 * 3600 },
	};
	const char *p;
	long tzhour = 0, tzmin = 0;
	size_t i;

	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++)
			tzhour = (tzhour * 10) + (*p - '0');
		if (*p == ':')
			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++)
			;
		if (i != 3)
			return 0;
		/* 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;
		}
	}
	return 0;
}

/* Parse time string `s` into the UNIX timestamp `tp`.
 * Returns 0 on success or -1 on failure. */
static int
parsetime(const char *s, long long *tp)
{
	static const struct {
		char *name;
		int len;
	} mons[] = {
		{ STRP("January"),   },
		{ STRP("February"),  },
		{ STRP("March"),     },
		{ STRP("April"),     },
		{ STRP("May"),       },
		{ STRP("June"),      },
		{ STRP("July"),      },
		{ STRP("August"),    },
		{ STRP("September"), },
		{ STRP("October"),   },
		{ STRP("November"),  },
		{ STRP("December"),  },
	};
	int va[6] = { 0 }, i, j, v, vi;
	size_t m;

	for (; ISSPACE((unsigned char)*s); 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])) {
		/* 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 (; ISSPACE((unsigned char)*s); s++)
			;
		if (*s == ',')
			s++;
		for (; ISSPACE((unsigned char)*s); s++)
			;
		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++)
			;
		/* end of word month */
		for (j = 0; ISALPHA((unsigned char)s[j]); j++)
			;
		/* check month name */
		if (j < 3 || j > 9)
			return -1; /* month cannot match */
		for (m = 0; m < sizeof(mons) / sizeof(*mons); m++) {
			/* abbreviation (3 length) or long name */
			if ((j == 3 || j == mons[m].len) &&
			    !strncasecmp(mons[m].name, s, j)) {
				va[1] = m + 1;
				s += j;
				break;
			}
		}
		if (m >= 12)
			return -1; /* no month found */
		for (; ISSPACE((unsigned char)*s); s++)
			;
		for (v = 0, i = 0; i < 4 && ISDIGIT((unsigned char)*s); s++, i++)
			v = (v * 10) + (*s - '0');
		/* 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++)
			;
		/* parse only regular time part, see below */
		vi = 3;
	}

	/* 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++) {
			v = (v * 10) + (*s - '0');
		}
		va[vi] = v;

		if ((vi < 2 && (*s == '-' || *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 == '.' || *s == ',') {
		for (s++; ISDIGIT((unsigned char)*s); s++)
			;
	}

	/* invalid range */
	if (va[0] < 0 || va[0] > 9999 ||
	    va[1] < 1 || va[1] > 12 ||
	    va[2] < 1 || va[2] > 31 ||
	    va[3] < 0 || va[3] > 23 ||
	    va[4] < 0 || va[4] > 59 ||
	    va[5] < 0 || va[5] > 60) /* allow leap second */
		return -1;

	*tp = datetounix(va[0] - 1900, va[1] - 1, va[2], va[3], va[4], va[5]) -
	      gettzoffset(s);

	return 0;
}

static void
printfields(void)
{
	string_print_timestamp(&ctx.fields[FeedFieldTime].str);
	putchar(FieldSeparator);
	string_print_trimmed(&ctx.fields[FeedFieldTitle].str);
	putchar(FieldSeparator);
	string_print_uri(&ctx.fields[FeedFieldLink].str);
	putchar(FieldSeparator);
	string_print_encoded(&ctx.fields[FeedFieldContent].str);
	putchar(FieldSeparator);
	fputs(contenttypes[ctx.contenttype], stdout);
	putchar(FieldSeparator);
	string_print_trimmed(&ctx.fields[FeedFieldId].str);
	putchar(FieldSeparator);
	string_print_trimmed(&ctx.fields[FeedFieldAuthor].str);
	putchar(FieldSeparator);
	string_print_uri(&ctx.fields[FeedFieldEnclosure].str);
	putchar(FieldSeparator);
	string_print_trimmed_multi(&ctx.fields[FeedFieldCategory].str);
	putchar('\n');

	if (ferror(stdout)) /* check for errors but do not flush */
		checkfileerror(stdout, "", 'w');
}

static int
istag(const char *name, size_t len, const char *name2, size_t len2)
{
	return (len == len2 && !strcasecmp(name, name2));
}

static int
isattr(const char *name, size_t len, const char *name2, size_t len2)
{
	return (len == len2 && !strcasecmp(name, name2));
}

static void
xmlattr(XMLParser *p, const char *t, size_t tl, const char *n, size_t nl,
	const char *v, size_t vl)
{
	/* handles transforming inline XML to data */
	if (ISINCONTENT(ctx)) {
		if (ctx.contenttype == ContentTypeHTML)
			xmldata(p, v, vl);
		return;
	}

	if (!ctx.tag.id)
		return;

	/* 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);
		return;
	}

	if (ctx.feedtype == FeedTypeRSS) {
		if (ctx.tag.id == RSSTagEnclosure &&
		    isattr(n, nl, STRP("url"))) {
			string_append(&tmpstr, v, vl);
		} else if (ctx.tag.id == RSSTagGuid &&
		           isattr(n, nl, STRP("ispermalink"))) {
			string_append(&attrispermalink, v, vl);
		}
	} else if (ctx.feedtype == FeedTypeAtom) {
		if (ctx.tag.id == AtomTagLink) {
			if (isattr(n, nl, STRP("rel"))) {
				string_append(&attrrel, v, vl);
			} else if (isattr(n, nl, STRP("href"))) {
				string_append(&tmpstr, v, vl);
			}
		} else if (ctx.tag.id == AtomTagCategory &&
			   isattr(n, nl, STRP("term"))) {
			string_append(&tmpstr, v, vl);
		}
	}
}

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[8];
	int len;

	/* handles transforming inline XML to data */
	if (ISINCONTENT(ctx)) {
		if (ctx.contenttype == ContentTypeHTML)
			xmldata(p, data, datalen);
		return;
	}

	if (!ctx.tag.id)
		return;

	/* try to translate entity, else just pass as data to
	 * xmlattr handler. */
	if ((len = xml_entitytostr(data, buf, sizeof(buf))) > 0)
		xmlattr(p, t, tl, n, nl, buf, (size_t)len);
	else
		xmlattr(p, t, tl, n, nl, data, datalen);
}

static void
xmlattrend(XMLParser *p, const char *t, size_t tl, const char *n, size_t nl)
{
	if (ISINCONTENT(ctx)) {
		if (ctx.contenttype == ContentTypeHTML) {
			/* handles transforming inline XML to data */
			xmldata(p, "\"", 1);
			ctx.attrcount = 0;
		}
		return;
	}
}

static void
xmlattrstart(XMLParser *p, const char *t, size_t tl, const char *n, size_t nl)
{
	if (ISINCONTENT(ctx)) {
		if (ctx.contenttype == ContentTypeHTML) {
			/* handles transforming inline XML to data */
			if (!ctx.attrcount)
				xmldata(p, " ", 1);
			ctx.attrcount++;
			xmldata(p, n, nl);
			xmldata(p, "=\"", 2);
		}
		return;
	}

	if (attrispermalink.len && isattr(n, nl, STRP("ispermalink")))
		string_clear(&attrispermalink);
	else if (attrrel.len && isattr(n, nl, STRP("rel")))
		string_clear(&attrrel);
	else if (attrtype.len && isattr(n, nl, STRP("type")))
		string_clear(&attrtype);
	else if (tmpstr.len &&
	    (isattr(n, nl, STRP("href")) ||
	     isattr(n, nl, STRP("term")) ||
	     isattr(n, nl, STRP("url"))))
		string_clear(&tmpstr); /* use the last value for multiple attribute values */
}

static void
xmldata(XMLParser *p, const char *s, size_t len)
{
	if (!ctx.field)
		return;

	if (ISFEEDFIELDMULTI(fieldmap[ctx.tag.id]))
		string_append(&tmpstr, s, len);
	else
		string_append(ctx.field, s, len);
}

static void
xmldataentity(XMLParser *p, const char *data, size_t datalen)
{
	char buf[8];
	int len;

	if (!ctx.field)
		return;

	/* try to translate entity, else just pass as data to
	 * xmldata handler. */
	if ((len = xml_entitytostr(data, buf, sizeof(buf))) > 0)
		xmldata(p, buf, (size_t)len);
	else
		xmldata(p, data, datalen);
}

static void
xmltagstart(XMLParser *p, const char *t, size_t tl)
{
	const FeedTag *f;

	if (ISINCONTENT(ctx)) {
		if (ctx.contenttype == ContentTypeHTML) {
			ctx.attrcount = 0;
			xmldata(p, "<", 1);
			xmldata(p, t, tl);
		}
		return;
	}

	/* start of RSS or Atom item / entry */
	if (ctx.feedtype == FeedTypeNone) {
		if (istag(t, tl, STRP("entry")))
			ctx.feedtype = FeedTypeAtom;
		else if (istag(t, tl, STRP("item")))
			ctx.feedtype = FeedTypeRSS;
		return;
	}

	/* field tagid already set or nested tags. */
	if (ctx.tag.id) {
		/* nested  for Atom */
		if (ctx.tag.id == AtomTagAuthor &&
		    istag(t, tl, STRP("name"))) {
			memcpy(&(ctx.tag), &atomtagauthorname, sizeof(ctx.tag));
		} else {
			return; /* other nested tags are not allowed: return */
		}
	}

	/* in item */
	if (ctx.tag.id == TagUnknown) {
		if (!(f = gettag(ctx.feedtype, t, tl)))
			f = ¬ag;
		memcpy(&(ctx.tag), f, sizeof(ctx.tag));
	}

	ctx.iscontenttag = (fieldmap[ctx.tag.id] == FeedFieldContent);
	string_clear(&attrispermalink);
	string_clear(&attrrel);
	string_clear(&attrtype);
}

static void
xmltagstartparsed(XMLParser *p, const char *t, size_t tl, int isshort)
{
	enum TagId tagid;

	if (ISINCONTENT(ctx)) {
		if (ctx.contenttype == ContentTypeHTML) {
			if (isshort)
				xmldata(p, "/>", 2);
			else
				xmldata(p, ">", 1);
		}
		return;
	}

	/* set tag type based on its attribute value */
	if (ctx.tag.id == RSSTagGuid) {
		/* if empty the default is "true" */
		if (!attrispermalink.len ||
		    isattr(attrispermalink.data, attrispermalink.len, STRP("true")))
			ctx.tag.id = RSSTagGuidPermalinkTrue;
		else
			ctx.tag.id = RSSTagGuidPermalinkFalse;
	} else if (ctx.tag.id == AtomTagLink) {
		/* empty or "alternate": other types could be
		 * "enclosure", "related", "self" or "via" */
		if (!attrrel.len || isattr(attrrel.data, attrrel.len, STRP("alternate")))
			ctx.tag.id = AtomTagLinkAlternate;
		else if (isattr(attrrel.data, attrrel.len, STRP("enclosure")))
			ctx.tag.id = AtomTagLinkEnclosure;
		else
			ctx.tag.id = AtomTagLink; /* unknown */
	}

	tagid = ctx.tag.id;

	/* map tag type to field: unknown or lesser priority is ignored,
	 * when tags of the same type are repeated only the first is used. */
	if (fieldmap[tagid] == -1 ||
	    (!ISFEEDFIELDMULTI(fieldmap[tagid]) &&
	     tagid <= ctx.fields[fieldmap[tagid]].tagid)) {
		return;
	}

	if (ctx.iscontenttag) {
		ctx.iscontent = 1;
		ctx.iscontenttag = 0;

		/* detect content-type based on type attribute */
		if (attrtype.len) {
			if (isattr(attrtype.data, attrtype.len, STRP("html")) ||
			    isattr(attrtype.data, attrtype.len, STRP("xhtml")) ||
			    isattr(attrtype.data, attrtype.len, STRP("text/html")) ||
			    isattr(attrtype.data, attrtype.len, STRP("text/xhtml")) ||
			    isattr(attrtype.data, attrtype.len, STRP("application/xhtml+xml")))
				ctx.contenttype = ContentTypeHTML;
			else /* unknown: handle as base64 text data */
				ctx.contenttype = ContentTypePlain;
		} else {
			/* default content-type */
			if (tagid == RSSTagContentEncoded || tagid == RSSTagDescription)
				ctx.contenttype = ContentTypeHTML;
			else
				ctx.contenttype = ContentTypePlain;
		}
	}

	ctx.field = &(ctx.fields[fieldmap[tagid]].str);
	ctx.fields[fieldmap[tagid]].tagid = tagid;

	/* clear field if it is overwritten (with a priority order) for the new
	 * value, if the field can have multiple values then do not clear it. */
	if (!ISFEEDFIELDMULTI(fieldmap[ctx.tag.id]))
		string_clear(ctx.field);
}

static void
xmltagend(XMLParser *p, const char *t, size_t tl, int isshort)
{
	size_t i;

	if (ctx.feedtype == FeedTypeNone)
		return;

	if (ISINCONTENT(ctx)) {
		/* not a closed content field */
		if (!istag(ctx.tag.name, ctx.tag.len, t, tl)) {
			if (!isshort && ctx.contenttype == ContentTypeHTML) {
				xmldata(p, "", 1);
			}
			return;
		}
	} 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 priority. */
		if (ctx.tag.id == RSSTagGuidPermalinkTrue && ctx.field &&
		    ctx.tag.id > ctx.fields[FeedFieldLink].tagid) {
			string_clear(&ctx.fields[FeedFieldLink].str);
			string_append(&ctx.fields[FeedFieldLink].str,
			              ctx.field->data, ctx.field->len);
			ctx.fields[FeedFieldLink].tagid = ctx.tag.id;
		}
	} else if (!ctx.tag.id && ((ctx.feedtype == FeedTypeAtom &&
	   istag(t, tl, STRP("entry"))) || /* Atom */
	   (ctx.feedtype == FeedTypeRSS &&
	   istag(t, tl, STRP("item"))))) /* RSS */
	{
		/* end of RSS or Atom entry / item */
		printfields();

		/* clear strings */
		for (i = 0; i < FeedFieldLast; i++) {
			string_clear(&ctx.fields[i].str);
			ctx.fields[i].tagid = TagUnknown;
		}
		ctx.contenttype = ContentTypeNone;
		/* allow parsing of Atom and RSS concatenated in one XML stream. */
		ctx.feedtype = FeedTypeNone;
	} else {
		return; /* not end of field */
	}

	/* temporary string: for fields that cannot be processed
	 * 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])) {
			if (ctx.field->len)
				string_append(ctx.field, FieldMultiSeparator, 1);
			string_append(ctx.field, tmpstr.data, tmpstr.len);
		} else {
			string_clear(ctx.field);
			string_append(ctx.field, tmpstr.data, tmpstr.len);
		}
	}

	/* close field */
	string_clear(&tmpstr); /* reuse and clear temporary string */

	if (ctx.tag.id == AtomTagAuthorName)
		memcpy(&(ctx.tag), &atomtagauthor, sizeof(ctx.tag)); /* outer tag */
	else
		memcpy(&(ctx.tag), ¬ag, sizeof(ctx.tag));

	ctx.iscontent = 0;
	ctx.field = NULL;
}

int
main(int argc, char *argv[])
{
	if (pledge("stdio", NULL) == -1)
		err(1, "pledge");

	if (argc > 1) {
		if (uri_parse(argv[1], &baseuri) != -1 && baseuri.proto[0])
			baseurl = argv[1];
		else
			errx(1, "baseurl incorrect or too long");
	}

	memcpy(&(ctx.tag), ¬ag, sizeof(ctx.tag));

	parser.xmlattr = xmlattr;
	parser.xmlattrentity = xmlattrentity;
	parser.xmlattrend = xmlattrend;
	parser.xmlattrstart = xmlattrstart;
	parser.xmlcdata = xmldata;
	parser.xmldata = xmldata;
	parser.xmldataentity = xmldataentity;
	parser.xmltagend = xmltagend;
	parser.xmltagstart = xmltagstart;
	parser.xmltagstartparsed = xmltagstartparsed;

	/* NOTE: GETNEXT is defined in xml.h for inline optimization */
	xml_parse(&parser);

	checkfileerror(stdin, "", 'r');
	checkfileerror(stdout, "", 'w');

	return 0;
}
sfeed-2.2/sfeed_atom.1000066400000000000000000000014241476411343700146460ustar00rootroot00000000000000.Dd July 31, 2021
.Dt SFEED_ATOM 1
.Os
.Sh NAME
.Nm sfeed_atom
.Nd format feed data to an Atom 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 an Atom (XML) feed.
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
sfeed_atom ~/.sfeed/feeds/*
.Ed
.Sh SEE ALSO
.Xr sfeed 1 ,
.Xr sfeed_plain 1 ,
.Xr sfeed 5
.Sh AUTHORS
.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org
sfeed-2.2/sfeed_atom.c000066400000000000000000000101501476411343700147240ustar00rootroot00000000000000#include 
#include 
#include 

#include "util.h"

static struct tm tmnow;
static time_t now;
static char *line;
static size_t linesize;

static void
printcontent(const char *s)
{
	for (; *s; ++s) {
		switch (*s) {
		case '<':  fputs("<",   stdout); break;
		case '>':  fputs(">",   stdout); break;
		case '\'': fputs("'",  stdout); break;
		case '&':  fputs("&",  stdout); break;
		case '"':  fputs(""", stdout); break;
		case '\\':
			if (*(s + 1) == '\0')
				break;
			s++;
			switch (*s) {
			case 'n':  putchar('\n'); break;
			case '\\': putchar('\\'); break;
			case 't':  putchar('\t'); break;
			}
			break;
		default:  putchar(*s);
		}
	}
}

static void
printfeed(FILE *fp, const char *feedname)
{
	char *fields[FieldLast], *p, *tmp;
	struct tm parsedtm, *tm;
	time_t parsedtime;
	ssize_t linelen;
	int c;

	while ((linelen = getline(&line, &linesize, fp)) > 0 &&
	       !ferror(stdout)) {
		if (line[linelen - 1] == '\n')
			line[--linelen] = '\0';
		parseline(line, fields);

		fputs("\n\t", stdout);
		if (feedname[0]) {
			fputs("[", stdout);
			xmlencode(feedname, stdout);
			fputs("] ", stdout);
		}
		xmlencode(fields[FieldTitle], stdout);
		fputs("\n", stdout);
		if (fields[FieldLink][0]) {
			fputs("\t\n", stdout);
		}
		/* prefer link over id for Atom . */
		fputs("\t", stdout);
		if (fields[FieldLink][0])
			xmlencode(fields[FieldLink], stdout);
		else if (fields[FieldId][0])
			xmlencode(fields[FieldId], stdout);
		fputs("\n", stdout);
		if (fields[FieldEnclosure][0]) {
			fputs("\t\n", stdout);
		}

		parsedtime = 0;
		if (strtotime(fields[FieldUnixTimestamp], &parsedtime) ||
		    !(tm = gmtime_r(&parsedtime, &parsedtm)))
			tm = &tmnow;
		fprintf(stdout, "\t%04d-%02d-%02dT%02d:%02d:%02dZ\n",
		        tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
		        tm->tm_hour, tm->tm_min, tm->tm_sec);

		if (fields[FieldAuthor][0]) {
			fputs("\t", stdout);
			xmlencode(fields[FieldAuthor], stdout);
			fputs("\n", stdout);
		}
		if (fields[FieldContent][0]) {
			if (!strcmp(fields[FieldContentType], "html")) {
				fputs("\t", stdout);
			} else {
				/* NOTE: an RSS/Atom viewer may or may not format
				 * whitespace such as newlines.
				 * Workaround: type="html" and 
]]> */ fputs("\t", stdout); } printcontent(fields[FieldContent]); fputs("\n", stdout); } for (p = fields[FieldCategory]; (tmp = strchr(p, '|')); p = tmp + 1) { c = *tmp; *tmp = '\0'; /* temporary NUL-terminate */ if (*p) { fputs("\t\n", stdout); } *tmp = c; /* restore */ } fputs("\n", stdout); } } int main(int argc, char *argv[]) { struct tm *tm; FILE *fp; char *name; int i; if (pledge(argc == 1 ? "stdio" : "stdio rpath", NULL) == -1) err(1, "pledge"); if ((now = time(NULL)) == (time_t)-1) errx(1, "time"); if (!(tm = gmtime_r(&now, &tmnow))) err(1, "gmtime_r: can't get current time"); fputs("\n" "\n" "\tNewsfeed\n", stdout); printf("\turn:newsfeed:%lld\n" "\t%04d-%02d-%02dT%02d:%02d:%02dZ\n", (long long)now, tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); if (argc == 1) { printfeed(stdin, ""); checkfileerror(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, "", 'w'); fclose(fp); } } fputs("\n", stdout); checkfileerror(stdout, "", 'w'); return 0; } sfeed-2.2/sfeed_content000077500000000000000000000023741476411343700152310ustar00rootroot00000000000000#!/bin/sh # Content viewer for sfeed(5) lines. # The locale is set to "C" for performance. The input is always UTF-8. LC_ALL=C awk -F '\t' ' function unescape(s) { # use the character "\x01" as a temporary replacement for "\". gsub("\\\\\\\\", "\x01", s); gsub("\\\\n", "\n", s); gsub("\\\\t", "\t", s); gsub("\x01", "\\", s); # restore "\x01" to "\". return s; } BEGIN { htmlconv = ENVIRON["SFEED_HTMLCONV"]; if (!length(htmlconv)) htmlconv = "lynx -stdin -dump " \ "-underline_links -image_links " \ "-display_charset=\"utf-8\" -assume_charset=\"utf-8\" "; } { if (previtem) print "\f"; previtem = 1; print "Title: " $2; if (length($7)) print "Author: " $7; if (length($9)) { categories = $9; gsub("\\|", ", ", categories); print "Category: " categories; } if (length($3)) print "Link: " $3; if (length($8)) print "Enclosure: " $8; if (!length($4)) next; print ""; if ($5 == "html") { # use the link of the item as the base URL for relative URLs in # HTML content. base = $3; if (length(base)) { gsub("\"", "%22", base); # encode quotes. base = "\n"; } print base unescape($4) | htmlconv; close(htmlconv); } else { print unescape($4); } }' "$@" | \ ${PAGER:-less -R} sfeed-2.2/sfeed_content.1000066400000000000000000000025321476411343700153610ustar00rootroot00000000000000.Dd October 27, 2024 .Dt SFEED_CONTENT 1 .Os .Sh NAME .Nm sfeed_content .Nd view RSS/Atom content .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 plain-text content. For HTML content it uses .Xr lynx 1 to convert it to plain-text. At the end it uses the pager to view the output. The .Nm script can be used by .Xr sfeed_curses 1 to view content. .Sh ENVIRONMENT VARIABLES .Bl -tag -width Ds .It Ev PAGER The pager used to view the content. If it is not set it will use "less -R" by default. .It Ev SFEED_HTMLCONV The program used to convert HTML content to plain-text. If it is not set it will use lynx by default. .El .Sh EXIT STATUS .Ex -std .Sh EXAMPLES Example: .Bd -literal -offset 4n curl -s 'https://codemadness.org/atom_content.xml' | sfeed | sfeed_content .Ed .Pp The output format will look like this: .Bd -literal -offset 4n Title: The title. Author: The line with the author if it is set. Category: The line with the categories if it is set. Link: The line with the link if it is set. Enclosure: The line with the enclosure if it is set. The content converted to plain-text.
if there are multiple items. .Ed .Sh SEE ALSO .Xr awk 1 , .Xr less 1 , .Xr lynx 1 , .Xr sfeed_curses 1 .Sh AUTHORS .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org sfeed-2.2/sfeed_curses.1000066400000000000000000000250131476411343700152120ustar00rootroot00000000000000.Dd October 27, 2024 .Dt SFEED_CURSES 1 .Os .Sh NAME .Nm sfeed_curses .Nd curses UI for viewing feed data .Sh SYNOPSIS .Nm .Op Ar .Sh DESCRIPTION .Nm formats feed data (TSV) from .Xr sfeed 1 from stdin or for each .Ar file into a curses UI. 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 such as the feeds sidebar. The .Ar file arguments are processed and shown in the specified argument order in the feeds sidebar. If no .Ar file arguments are specified then the data is read from stdin and the feed name is "stdin" and no sidebar is visible by default in this case. .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. .Pp .Nm aligns the output. Make sure the environment variable .Ev LC_CTYPE is set to a UTF-8 locale, so it can determine the proper column-width per rune, using .Xr mbtowc 3 and .Xr wcwidth 3 . .Sh KEYBINDS .Bl -tag -width Ds .It k, ARROW UP Go one row up. .It j, ARROW DOWN Go one row down. .It K Go to the previous bold row. .It J Go to the next bold row. .It h, ARROW LEFT Focus feeds pane. .It l, ARROW RIGHT Focus items pane. .It TAB Cycle focused pane (between feeds and items). .It g Go to the first row. .It G Go to the last row. .It PAGE UP, CTRL-B Scroll one page up. .It PAGE DOWN, CTRL-F, SPACE Scroll one page down. .It / Prompt for a new search and search forward (case-insensitive). .It ? Prompt for a new search and search backward (case-insensitive). .It n Search forward with the previously set search term. .It N Search backward with the previously set search term. .It \&[ Go to the previous feed in the feeds pane and open it. .It ] Go to the next feed in the feeds pane and open it. .It CTRL-L 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. .It s Toggle between monocle layout and the previous non-monocle layout. .It < Use a fixed sidebar size for the current layout and decrease the fixed width or height by 1 column. .It > 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 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 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. .It a, e, @ Plumb URL of the enclosure. The URL is passed as a parameter to the program specified in .Ev SFEED_PLUMBER . .It o, ENTER, RETURN Feeds pane: load feed and its items. In the monocle layout it will also switch to the items pane after loading the feed items. Items pane: plumb current item URL, the URL is passed as a parameter to the program specified in .Ev SFEED_PLUMBER . .It c, p, | Pipe the whole TAB-Separated Value line to a program. This program can be specified with .Ev SFEED_PIPER . .It y Pipe the TAB-Separated Value field for yanking the URL to a program. This program can be specified with .Ev SFEED_YANKER . .It E Pipe the TAB-Separated Value field for yanking the enclosure to a program. This program can be specified with .Ev SFEED_YANKER . .It r Mark item as read. This will only work when .Ev SFEED_URL_FILE is set. .It u Mark item as unread. This will only work when .Ev SFEED_URL_FILE is set. .It f 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 currently loaded feed as unread. This will only work when .Ev SFEED_URL_FILE is set. .It 1 Set the current layout to a vertical mode. Showing a feeds sidebar to the left and the feed items to the right. .It 2 Set the current layout to a horizontal mode. Showing a feeds sidebar on the top and the feed items on the bottom. .It 3 Set the current layout to a monocle mode. Showing either a feeds or a feed items pane. .It q, EOF Quit .El .Sh MOUSE ACTIONS When mouse-mode is enabled the below actions are available. .Bl -tag -width Ds .It LEFT-CLICK Feeds pane: select and load the feed and its items. In the monocle layout it will also switch to the items pane after loading the feed items. Items pane: select item, when already selected then plumb it. .It RIGHT-CLICK Feeds pane: select feed, but do not load it. Items pane: pipe the item. .It SCROLL UP Scroll one page up. .It SCROLL DOWN Scroll one page down. .It FORWARD Switch to the items pane. .It BACKWARD Switch to the feeds pane. .El .Sh SIGNALS .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: 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. 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. 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 clipboard. By default this is "xclip -r". .It Ev SFEED_YANKER_INTERACTIVE Handle the program interactively in the same terminal or not. 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, instead of checking the timestamp, which is the default. The value specified is a plain-text file containing a list of read URLs, one URL per line. This URL is matched on the link field if it is set, otherwise it is matched on the id field. .It Ev SFEED_MARK_READ A program to mark items as read if .Ev SFEED_URL_FILE is also set, if unset the default program used is "sfeed_markread read". The marked items are piped to the program line by line. If the feed item has a link then this line is the link field, otherwise it is the id field. The program is expected to merge items in a safe/transactional manner. The program should return the exit status 0 on success or non-zero on failure. .It Ev SFEED_MARK_UNREAD A program to mark items as unread if .Ev SFEED_URL_FILE is also set, if unset the default program used is "sfeed_markread unread". The unmarked items are piped to the program line by line. If the feed item has a link then this line is the link field, otherwise it is the id field. The program is expected to merge items in a safe/transactional manner. The program should return the exit status 0 on success or non-zero on failure. .It Ev SFEED_LAZYLOAD Lazyload items when reading the feed data from files. This can reduce memory usage but increases latency when seeking items, especially on slower disk drives. 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 after the data was updated. This makes .Nm 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 .Nm 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 Example: .Bd -literal -offset 4n sfeed_curses ~/.sfeed/feeds/* .Ed .Pp Another example which shows some of the features .Nm has: .Bd -literal -offset 4n export SFEED_AUTOCMD="2tgo" export SFEED_URL_FILE="$HOME/.sfeed/urls" [ -f "$SFEED_URL_FILE" ] || touch "$SFEED_URL_FILE" sfeed_curses ~/.sfeed/feeds/* .Ed .Pp Which does the following: .Bl -enum .It Set commands to execute automatically on startup: .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. .Pp Toggle showing only feeds with new items in the sidebar ('t' keybind). .Pp Go to the first row in the current panel ('g' keybind). .Pp Load the currently selected feed ('o' keybind). .It Set a file to use for managing read and unread items. 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 and read the specified feed files. .El .Sh SEE ALSO .Xr sfeed 1 , .Xr sfeed_content 1 , .Xr sfeed_markread 1 , .Xr sfeed_plain 1 , .Xr xclip 1 , .Xr sfeed 5 .Sh AUTHORS .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org sfeed-2.2/sfeed_curses.c000066400000000000000000001472041476411343700153030ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "util.h" /* curses */ #ifndef SFEED_MINICURSES #include #include #else #include "minicurses.h" #endif #define LEN(a) sizeof((a))/sizeof((a)[0]) #define MAX(a,b) ((a) > (b) ? (a) : (b)) #define MIN(a,b) ((a) < (b) ? (a) : (b)) #ifndef SFEED_DUMBTERM #define SCROLLBAR_SYMBOL_BAR "\xe2\x94\x82" /* symbol: "light vertical" */ #define SCROLLBAR_SYMBOL_TICK " " #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 "|" #define SCROLLBAR_SYMBOL_TICK " " #define LINEBAR_SYMBOL_BAR "-" #define LINEBAR_SYMBOL_RIGHT "|" #endif /* color-theme */ #ifndef SFEED_THEME #define SFEED_THEME "themes/mono.h" #endif #include SFEED_THEME enum { ATTR_RESET = 0, ATTR_BOLD_ON = 1, ATTR_FAINT_ON = 2, ATTR_REVERSE_ON = 7 }; enum Layout { LayoutVertical = 0, LayoutHorizontal, LayoutMonocle, LayoutLast }; enum Pane { PaneFeeds, PaneItems, PaneLast }; struct win { int width; /* absolute width of the window */ int height; /* absolute height of the window */ int dirty; /* needs draw update: clears screen */ }; struct row { char *text; /* text string, optional if using row_format() callback */ int bold; void *data; /* data binding */ }; struct pane { int x; /* absolute x position on the screen */ int y; /* absolute y position on the screen */ int width; /* absolute width of the pane */ int height; /* absolute height of the pane, should be > 0 */ off_t pos; /* focused row position */ struct row *rows; size_t nrows; /* total amount of rows */ int focused; /* has focus or not */ int hidden; /* is visible or not */ int dirty; /* needs draw update */ /* (optional) callback functions */ struct row *(*row_get)(struct pane *, off_t); char *(*row_format)(struct pane *, struct row *); int (*row_match)(struct pane *, struct row *, const char *); }; struct scrollbar { int tickpos; int ticksize; int x; /* absolute x position on the screen */ int y; /* absolute y position on the screen */ int size; /* absolute size of the bar, should be > 0 */ int focused; /* has focus or not */ int hidden; /* is visible or not */ int dirty; /* needs draw update */ }; struct statusbar { int x; /* absolute x position on the screen */ int y; /* absolute y position on the screen */ int width; /* absolute width of the bar */ char *text; /* data */ int hidden; /* is visible or not */ int dirty; /* needs draw update */ }; struct linebar { int x; /* absolute x position on the screen */ int y; /* absolute y position on the screen */ int width; /* absolute width of the line */ int hidden; /* is visible or not */ int dirty; /* needs draw update */ }; /* /UI */ struct item { char *fields[FieldLast]; char *line; /* allocated split line */ /* field to match new items, if link is set match on link, else on id */ char *matchnew; time_t timestamp; int timeok; int isnew; 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 */ }; 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; static struct pane panes[PaneLast]; static struct scrollbar scrollbars[PaneLast]; /* each pane has a scrollbar */ static struct win win; static size_t selpane; /* fixed sidebar size, < 0 is automatic */ static int fixedsidebarsizes[LayoutLast] = { -1, -1, -1 }; static int layout = LayoutVertical, prevlayout = LayoutVertical; static int onlynew = 0; /* show only new in sidebar */ static int usemouse = 1; /* use xterm mouse tracking */ static struct termios tsave; /* terminal state at startup */ static struct termios tcur; static int devnullfd; static int istermsetup, needcleanup; static struct feed *feeds; static struct feed *curfeed; static size_t nfeeds; /* amount of feeds */ static time_t comparetime; static struct urls urls; static char *urlfile; 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 */ static char *yankercmd = "xclip -r"; /* env variable: $SFEED_YANKER */ static char *markreadcmd = "sfeed_markread read"; /* env variable: $SFEED_MARK_READ */ static char *markunreadcmd = "sfeed_markread unread"; /* env variable: $SFEED_MARK_UNREAD */ static char *cmdenv; /* env variable: $SFEED_AUTOCMD */ static int plumberia = 0; /* env variable: $SFEED_PLUMBER_INTERACTIVE */ 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 */ static int ttywritef(const char *fmt, ...) { va_list ap; int n; va_start(ap, fmt); n = vfprintf(stdout, fmt, ap); va_end(ap); fflush(stdout); return n; } static int ttywrite(const char *s) { if (!s) return 0; /* for tparm() returning NULL */ return write(1, s, strlen(s)); } /* Print to stderr, call cleanup() and _exit(). */ __dead static void die(const char *fmt, ...) { va_list ap; int saved_errno; saved_errno = errno; cleanup(); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); if (saved_errno) fprintf(stderr, ": %s", strerror(saved_errno)); putc('\n', stderr); fflush(stderr); _exit(1); } static void * erealloc(void *ptr, size_t size) { void *p; if (!(p = realloc(ptr, size))) die("realloc"); return p; } static void * ecalloc(size_t nmemb, size_t size) { void *p; if (!(p = calloc(nmemb, size))) die("calloc"); return p; } static char * estrdup(const char *s) { char *p; if (!(p = strdup(s))) die("strdup"); return p; } /* Wrapper for tparm() which allows NULL parameter for str. */ static char * tparmnull(const char *str, long p1, long p2, long p3, long p4, long p5, long p6, long p7, long p8, long p9) { if (!str) return NULL; /* some tparm() implementations have char *, some have const char * */ return tparm((char *)str, p1, p2, p3, p4, p5, p6, p7, p8, p9); } /* Counts column width of character string. */ static size_t colw(const char *s) { wchar_t wc; size_t col = 0, i, slen; int inc, rl, w; slen = strlen(s); for (i = 0; i < slen; i += inc) { inc = 1; /* next byte */ if ((unsigned char)s[i] < 32) { continue; } else if ((unsigned char)s[i] >= 127) { rl = mbtowc(&wc, &s[i], slen - i < 4 ? slen - i : 4); inc = rl; if (rl < 0) { mbtowc(NULL, NULL, 0); /* reset state */ inc = 1; /* invalid, seek next byte */ w = 1; /* replacement char is one width */ } else if ((w = wcwidth(wc)) == -1) { continue; } col += w; } else { col++; } } return col; } /* Format `len` columns of characters. If string is shorter pad the rest * with characters `pad`. */ static int utf8pad(char *buf, size_t bufsiz, const char *s, size_t len, int pad) { wchar_t wc; size_t col = 0, i, slen, siz = 0; int inc, rl, w; if (!bufsiz) return -1; if (!len) { buf[0] = '\0'; return 0; } slen = strlen(s); for (i = 0; i < slen; i += inc) { inc = 1; /* next byte */ if ((unsigned char)s[i] < 32) continue; rl = mbtowc(&wc, &s[i], slen - i < 4 ? slen - i : 4); inc = rl; if (rl < 0) { mbtowc(NULL, NULL, 0); /* reset state */ inc = 1; /* invalid, seek next byte */ w = 1; /* replacement char is one width */ } else if ((w = wcwidth(wc)) == -1) { continue; } if (col + w > len || (col + w == len && s[i + inc])) { if (siz + 4 >= bufsiz) return -1; memcpy(&buf[siz], PAD_TRUNCATE_SYMBOL, sizeof(PAD_TRUNCATE_SYMBOL) - 1); siz += sizeof(PAD_TRUNCATE_SYMBOL) - 1; buf[siz] = '\0'; col++; break; } else if (rl < 0) { if (siz + 4 >= bufsiz) return -1; memcpy(&buf[siz], UTF_INVALID_SYMBOL, sizeof(UTF_INVALID_SYMBOL) - 1); siz += sizeof(UTF_INVALID_SYMBOL) - 1; buf[siz] = '\0'; col++; continue; } if (siz + inc + 1 >= bufsiz) return -1; memcpy(&buf[siz], &s[i], inc); siz += inc; buf[siz] = '\0'; col += w; } len -= col; if (siz + len + 1 >= bufsiz) return -1; memset(&buf[siz], pad, len); siz += len; buf[siz] = '\0'; return 0; } static void resetstate(void) { ttywrite("\x1b""c"); /* rs1: reset title and state */ } static void updatetitle(void) { unsigned long totalnew = 0, total = 0; size_t i; for (i = 0; i < nfeeds; i++) { totalnew += feeds[i].totalnew; total += feeds[i].total; } ttywritef("\x1b]2;(%lu/%lu) - sfeed_curses\x1b\\", totalnew, total); } static void appmode(int on) { ttywrite(tparmnull(on ? enter_ca_mode : exit_ca_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0)); } 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 */ } static void cursormode(int on) { ttywrite(tparmnull(on ? cursor_normal : cursor_invisible, 0, 0, 0, 0, 0, 0, 0, 0, 0)); } static void cursormove(int x, int y) { ttywrite(tparmnull(cursor_address, y, x, 0, 0, 0, 0, 0, 0, 0)); } static void cursorsave(void) { /* do not save the cursor if it won't be restored anyway */ if (cursor_invisible) ttywrite(tparmnull(save_cursor, 0, 0, 0, 0, 0, 0, 0, 0, 0)); } static void cursorrestore(void) { /* if the cursor cannot be hidden then move to a consistent position */ if (cursor_invisible) ttywrite(tparmnull(restore_cursor, 0, 0, 0, 0, 0, 0, 0, 0, 0)); else cursormove(0, 0); } static void attrmode(int mode) { switch (mode) { case ATTR_RESET: ttywrite(tparmnull(exit_attribute_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0)); break; case ATTR_BOLD_ON: ttywrite(tparmnull(enter_bold_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0)); break; case ATTR_FAINT_ON: ttywrite(tparmnull(enter_dim_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0)); break; case ATTR_REVERSE_ON: ttywrite(tparmnull(enter_reverse_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0)); break; default: break; } } static void cleareol(void) { ttywrite(tparmnull(clr_eol, 0, 0, 0, 0, 0, 0, 0, 0, 0)); } static void clearscreen(void) { ttywrite(tparmnull(clear_screen, 0, 0, 0, 0, 0, 0, 0, 0, 0)); } static void cleanup(void) { struct sigaction sa; if (!needcleanup) return; needcleanup = 0; if (istermsetup) { resetstate(); cursormode(1); appmode(0); clearscreen(); if (usemouse) mousemode(0); } /* restore terminal settings */ tcsetattr(0, TCSANOW, &tsave); memset(&sa, 0, sizeof(sa)); sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; /* require BSD signal semantics */ sa.sa_handler = SIG_DFL; sigaction(SIGWINCH, &sa, NULL); } static void win_update(struct win *w, int width, int height) { if (width != w->width || height != w->height) w->dirty = 1; w->width = width; w->height = height; } static void resizewin(void) { struct winsize winsz; int width, height; if (ioctl(1, TIOCGWINSZ, &winsz) != -1) { width = winsz.ws_col > 0 ? winsz.ws_col : 80; height = winsz.ws_row > 0 ? winsz.ws_row : 24; win_update(&win, width, height); } if (win.dirty) alldirty(); } static void init(void) { struct sigaction sa; int errret = 1; needcleanup = 1; tcgetattr(0, &tsave); memcpy(&tcur, &tsave, sizeof(tcur)); tcur.c_lflag &= ~(ECHO|ICANON); tcur.c_cc[VMIN] = 1; tcur.c_cc[VTIME] = 0; tcsetattr(0, TCSANOW, &tcur); if (!istermsetup && (setupterm(NULL, 1, &errret) != OK || errret != 1)) { errno = 0; die("setupterm: terminfo database or entry for $TERM not found"); } istermsetup = 1; resizewin(); appmode(1); cursormode(0); if (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); } static void processexit(pid_t pid, int interactive) { struct sigaction sa; if (interactive) { 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(); } } /* Pipe item line or item field to a program. * If `field` is -1 then pipe the TSV line, else a specified field. * 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. */ static void pipeitem(const char *cmd, struct item *item, int field, int interactive) { FILE *fp; pid_t pid; int i, status; if (interactive) cleanup(); switch ((pid = fork())) { case -1: die("fork"); case 0: if (!interactive) { dup2(devnullfd, 1); /* stdout */ dup2(devnullfd, 2); /* stderr */ } errno = 0; if (!(fp = popen(cmd, "w"))) die("popen: %s", cmd); if (field == -1) { for (i = 0; i < FieldLast; i++) { if (i) putc('\t', fp); fputs(item->fields[i], fp); } } else { fputs(item->fields[field], fp); } putc('\n', fp); status = pclose(fp); status = WIFEXITED(status) ? WEXITSTATUS(status) : 127; _exit(status); default: processexit(pid, interactive); } } static void forkexec(char *argv[], int interactive) { pid_t pid; if (interactive) cleanup(); switch ((pid = fork())) { case -1: die("fork"); case 0: if (!interactive) { dup2(devnullfd, 0); /* stdin */ dup2(devnullfd, 1); /* stdout */ dup2(devnullfd, 2); /* stderr */ } if (execvp(argv[0], argv) == -1) _exit(1); default: processexit(pid, interactive); } } static struct row * pane_row_get(struct pane *p, off_t pos) { if (pos < 0 || pos >= p->nrows) return NULL; if (p->row_get) return p->row_get(p, pos); return p->rows + pos; } static char * pane_row_text(struct pane *p, struct row *row) { /* custom formatter */ if (p->row_format) return p->row_format(p, row); return row->text; } static int pane_row_match(struct pane *p, struct row *row, const char *s) { if (p->row_match) return p->row_match(p, row, s); return (strcasestr(pane_row_text(p, row), s) != NULL); } static void pane_row_draw(struct pane *p, off_t pos, int selected) { struct row *row; if (p->hidden || !p->width || !p->height || p->x >= win.width || p->y + (pos % p->height) >= win.height) return; row = pane_row_get(p, pos); cursorsave(); cursormove(p->x, p->y + (pos % p->height)); if (p->focused) THEME_ITEM_FOCUS(); else THEME_ITEM_NORMAL(); if (row && row->bold) THEME_ITEM_BOLD(); if (selected) THEME_ITEM_SELECTED(); if (row) { printutf8pad(stdout, pane_row_text(p, row), p->width, ' '); fflush(stdout); } else { ttywritef("%-*.*s", p->width, p->width, ""); } attrmode(ATTR_RESET); cursorrestore(); } static void pane_setpos(struct pane *p, off_t pos) { if (pos < 0) pos = 0; /* clamp */ if (!p->nrows) return; /* invalid */ if (pos >= p->nrows) pos = p->nrows - 1; /* clamp */ if (pos == p->pos) return; /* no change */ /* is on different scroll region? mark whole pane dirty */ if (((p->pos - (p->pos % p->height)) / p->height) != ((pos - (pos % p->height)) / p->height)) { p->dirty = 1; } else { /* only redraw the 2 dirty rows */ pane_row_draw(p, p->pos, 0); pane_row_draw(p, pos, 1); } p->pos = pos; } static void pane_scrollpage(struct pane *p, int pages) { off_t pos; if (pages < 0) { pos = p->pos - (-pages * p->height); pos -= (p->pos % p->height); pos += p->height - 1; pane_setpos(p, pos); } else if (pages > 0) { pos = p->pos + (pages * p->height); if ((p->pos % p->height)) pos -= (p->pos % p->height); pane_setpos(p, pos); } } static void pane_scrolln(struct pane *p, int n) { pane_setpos(p, p->pos + n); } static void pane_setfocus(struct pane *p, int on) { if (p->focused != on) { p->focused = on; p->dirty = 1; } } static void pane_draw(struct pane *p) { off_t pos, y; if (!p->dirty) return; p->dirty = 0; if (p->hidden || !p->width || !p->height) return; /* draw visible rows */ pos = p->pos - (p->pos % p->height); for (y = 0; y < p->height; y++) pane_row_draw(p, y + pos, (y + pos) == p->pos); } static void setlayout(int n) { if (layout != LayoutMonocle) prevlayout = layout; /* previous non-monocle layout */ layout = n; } static void updategeom(void) { int h, w, x = 0, y = 0; panes[PaneFeeds].hidden = layout == LayoutMonocle && (selpane != PaneFeeds); panes[PaneItems].hidden = layout == LayoutMonocle && (selpane != PaneItems); linebar.hidden = layout != LayoutHorizontal; w = win.width; /* always reserve space for statusbar */ h = MAX(win.height - 1, 1); panes[PaneFeeds].x = x; panes[PaneFeeds].y = y; switch (layout) { case LayoutVertical: panes[PaneFeeds].width = getsidebarsize(); x += panes[PaneFeeds].width; w -= panes[PaneFeeds].width; /* space for scrollbar if sidebar is visible */ w--; x++; panes[PaneFeeds].height = MAX(h, 1); break; case LayoutHorizontal: panes[PaneFeeds].height = getsidebarsize(); h -= panes[PaneFeeds].height; y += panes[PaneFeeds].height; linebar.x = 0; linebar.y = y; linebar.width = win.width; h--; y++; panes[PaneFeeds].width = MAX(w - 1, 0); break; case LayoutMonocle: panes[PaneFeeds].height = MAX(h, 1); panes[PaneFeeds].width = MAX(w - 1, 0); break; } panes[PaneItems].x = x; panes[PaneItems].y = y; panes[PaneItems].width = MAX(w - 1, 0); panes[PaneItems].height = MAX(h, 1); if (x >= win.width || y + 1 >= win.height) panes[PaneItems].hidden = 1; scrollbars[PaneFeeds].x = panes[PaneFeeds].x + panes[PaneFeeds].width; scrollbars[PaneFeeds].y = panes[PaneFeeds].y; scrollbars[PaneFeeds].size = panes[PaneFeeds].height; scrollbars[PaneFeeds].hidden = panes[PaneFeeds].hidden; scrollbars[PaneItems].x = panes[PaneItems].x + panes[PaneItems].width; scrollbars[PaneItems].y = panes[PaneItems].y; scrollbars[PaneItems].size = panes[PaneItems].height; scrollbars[PaneItems].hidden = panes[PaneItems].hidden; statusbar.width = win.width; statusbar.x = 0; statusbar.y = MAX(win.height - 1, 0); alldirty(); } static void scrollbar_setfocus(struct scrollbar *s, int on) { if (s->focused != on) { s->focused = on; s->dirty = 1; } } static void scrollbar_update(struct scrollbar *s, off_t pos, off_t nrows, int pageheight) { int tickpos = 0, ticksize = 0; /* do not show a scrollbar if all items fit on the page */ if (nrows > pageheight) { ticksize = s->size / ((double)nrows / (double)pageheight); if (ticksize == 0) ticksize = 1; tickpos = (pos / (double)nrows) * (double)s->size; /* fixup due to cell precision */ if (pos + pageheight >= nrows || tickpos + ticksize >= s->size) tickpos = s->size - ticksize; } if (s->tickpos != tickpos || s->ticksize != ticksize) s->dirty = 1; s->tickpos = tickpos; s->ticksize = ticksize; } static void scrollbar_draw(struct scrollbar *s) { off_t y; if (!s->dirty) return; s->dirty = 0; if (s->hidden || !s->size || s->x >= win.width || s->y >= win.height) return; cursorsave(); /* draw bar (not tick) */ if (s->focused) THEME_SCROLLBAR_FOCUS(); else THEME_SCROLLBAR_NORMAL(); for (y = 0; y < s->size; y++) { if (y >= s->tickpos && y < s->tickpos + s->ticksize) continue; /* skip tick */ cursormove(s->x, s->y + y); ttywrite(SCROLLBAR_SYMBOL_BAR); } /* draw tick */ if (s->focused) THEME_SCROLLBAR_TICK_FOCUS(); else THEME_SCROLLBAR_TICK_NORMAL(); for (y = s->tickpos; y < s->size && y < s->tickpos + s->ticksize; y++) { cursormove(s->x, s->y + y); ttywrite(SCROLLBAR_SYMBOL_TICK); } attrmode(ATTR_RESET); cursorrestore(); } static int readch(void) { unsigned char b; fd_set readfds; struct timeval tv; if (cmdenv && *cmdenv) { b = *(cmdenv++); /* $SFEED_AUTOCMD */ return (int)b; } for (;;) { FD_ZERO(&readfds); FD_SET(0, &readfds); tv.tv_sec = 0; tv.tv_usec = 250000; /* 250ms */ switch (select(1, &readfds, NULL, NULL, &tv)) { case -1: if (errno != EINTR) die("select"); return -2; /* EINTR: like a signal */ case 0: return -3; /* time-out */ } switch (read(0, &b, 1)) { case -1: die("read"); case 0: return EOF; default: return (int)b; } } } static char * lineeditor(void) { char *input = NULL; size_t cap = 0, nchars = 0; int ch; if (usemouse) mousemode(0); for (;;) { if (nchars + 2 >= cap) { cap = cap ? cap * 2 : 32; input = erealloc(input, cap); } ch = readch(); if (ch == EOF || ch == '\r' || ch == '\n') { input[nchars] = '\0'; break; } else if (ch == '\b' || ch == 0x7f) { if (!nchars) continue; input[--nchars] = '\0'; ttywrite("\b \b"); /* back, blank, back */ } else if (ch >= ' ') { input[nchars] = ch; input[nchars + 1] = '\0'; ttywrite(&input[nchars]); nchars++; } else if (ch < 0) { 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); input = NULL; break; /* cancel prompt */ } } if (usemouse) mousemode(1); return input; } static char * uiprompt(int x, int y, char *fmt, ...) { va_list ap; char *input, buf[32]; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); cursorsave(); cursormove(x, y); THEME_INPUT_LABEL(); ttywrite(buf); attrmode(ATTR_RESET); THEME_INPUT_NORMAL(); cleareol(); cursormode(1); cursormove(x + colw(buf) + 1, y); input = lineeditor(); attrmode(ATTR_RESET); cursormode(0); cursorrestore(); return input; } static void linebar_draw(struct linebar *b) { int i; if (!b->dirty) return; b->dirty = 0; if (b->hidden || !b->width) return; cursorsave(); cursormove(b->x, b->y); THEME_LINEBAR(); for (i = 0; i < b->width - 1; i++) ttywrite(LINEBAR_SYMBOL_BAR); ttywrite(LINEBAR_SYMBOL_RIGHT); attrmode(ATTR_RESET); cursorrestore(); } static void statusbar_draw(struct statusbar *s) { if (!s->dirty) return; s->dirty = 0; if (s->hidden || !s->width || s->x >= win.width || s->y >= win.height) return; cursorsave(); cursormove(s->x, s->y); THEME_STATUSBAR(); /* terminals without xenl (eat newline glitch) mess up scrolling when * using the last cell on the last line on the screen. */ printutf8pad(stdout, s->text, s->width - (!eat_newline_glitch), ' '); fflush(stdout); attrmode(ATTR_RESET); cursorrestore(); } static void statusbar_update(struct statusbar *s, const char *text) { if (s->text && !strcmp(s->text, text)) return; free(s->text); s->text = estrdup(text); s->dirty = 1; } /* Line to item, modifies and splits line in-place. */ static int linetoitem(char *line, struct item *item) { char *fields[FieldLast]; time_t parsedtime; item->line = line; parseline(line, fields); memcpy(item->fields, fields, sizeof(fields)); if (urlfile) item->matchnew = estrdup(fields[fields[FieldLink][0] ? FieldLink : FieldId]); else item->matchnew = NULL; parsedtime = 0; if (!strtotime(fields[FieldUnixTimestamp], &parsedtime)) { item->timestamp = parsedtime; item->timeok = 1; } else { item->timestamp = 0; item->timeok = 0; } return 0; } static void feed_items_free(struct items *items) { size_t i; for (i = 0; i < items->len; i++) { free(items->items[i].line); free(items->items[i].matchnew); } free(items->items); items->items = NULL; items->len = 0; items->cap = 0; } static void feed_items_get(struct feed *f, FILE *fp, struct items *itemsret) { struct item *item, *items = NULL; char *line = NULL; size_t cap, i, linesize = 0, nitems; ssize_t linelen, n; off_t offset; cap = nitems = 0; offset = 0; for (i = 0; ; i++) { if (i + 1 >= cap) { cap = cap ? cap * 2 : 16; items = erealloc(items, cap * sizeof(struct item)); } if ((n = linelen = getline(&line, &linesize, fp)) > 0) { item = &items[i]; item->offset = offset; offset += linelen; if (line[linelen - 1] == '\n') line[--linelen] = '\0'; if (lazyload && f->path) { linetoitem(line, item); /* data is ignored here, will be lazy-loaded later. */ item->line = NULL; memset(item->fields, 0, sizeof(item->fields)); } else { linetoitem(estrdup(line), item); } nitems++; } if (ferror(fp)) die("getline: %s", f->name); if (n <= 0 || feof(fp)) break; } itemsret->items = items; itemsret->len = nitems; itemsret->cap = cap; free(line); } static void updatenewitems(struct feed *f) { struct pane *p; struct row *row; struct item *item; 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_hasmatch(&urls, item->matchnew); else item->isnew = (item->timeok && item->timestamp >= comparetime); row->bold = item->isnew; f->totalnew += item->isnew; } f->total = p->nrows; } static void feed_load(struct feed *f, FILE *fp) { /* static, reuse local buffers */ static struct items items; struct pane *p; size_t i; feed_items_free(&items); feed_items_get(f, fp, &items); p = &panes[PaneItems]; p->pos = 0; p->nrows = items.len; free(p->rows); p->rows = ecalloc(sizeof(p->rows[0]), items.len + 1); for (i = 0; i < items.len; i++) p->rows[i].data = &(items.items[i]); /* do not use pane_row_get() */ updatenewitems(f); } static void feed_count(struct feed *f, FILE *fp) { char *fields[FieldLast]; char *line = NULL; size_t linesize = 0; ssize_t linelen; time_t parsedtime; f->totalnew = f->total = 0; while ((linelen = getline(&line, &linesize, fp)) > 0) { if (line[linelen - 1] == '\n') line[--linelen] = '\0'; parseline(line, fields); if (urlfile) { f->totalnew += !urls_hasmatch(&urls, fields[fields[FieldLink][0] ? FieldLink : FieldId]); } else { parsedtime = 0; if (!strtotime(fields[FieldUnixTimestamp], &parsedtime)) f->totalnew += (parsedtime >= comparetime); } f->total++; } if (ferror(fp)) die("getline: %s", f->name); free(line); } static void feed_setenv(struct feed *f) { if (f && f->path) setenv("SFEED_FEED_PATH", f->path, 1); else unsetenv("SFEED_FEED_PATH"); } /* Change feed, have one file open, reopen file if needed. */ static void feeds_set(struct feed *f) { if (curfeed) { if (curfeed->path && curfeed->fp) { fclose(curfeed->fp); curfeed->fp = NULL; } } if (f && f->path) { if (!f->fp && !(f->fp = fopen(f->path, "rb"))) die("fopen: %s", f->path); } feed_setenv(f); curfeed = f; } static void feeds_load(struct feed *feeds, size_t nfeeds) { struct feed *f; size_t i; errno = 0; if ((comparetime = getcomparetime()) == (time_t)-1) die("getcomparetime"); for (i = 0; i < nfeeds; i++) { f = &feeds[i]; if (f->path) { if (f->fp) { if (fseek(f->fp, 0, SEEK_SET)) die("fseek: %s", f->path); } else { if (!(f->fp = fopen(f->path, "rb"))) die("fopen: %s", f->path); } } if (!f->fp) { /* reading from stdin, just recount new */ if (f == curfeed) updatenewitems(f); continue; } /* load first items, because of first selection or stdin. */ if (f == curfeed) { feed_load(f, f->fp); } else { feed_count(f, f->fp); if (f->path && f->fp) { fclose(f->fp); f->fp = NULL; } } } } /* find row position of the feed if visible, else return -1 */ static off_t feeds_row_get(struct pane *p, struct feed *f) { struct row *row; struct feed *fr; off_t pos; for (pos = 0; pos < p->nrows; pos++) { if (!(row = pane_row_get(p, pos))) continue; fr = row->data; if (!strcmp(fr->name, f->name)) return pos; } return -1; } static void feeds_reloadall(void) { struct pane *p; struct feed *f = NULL; struct row *row; off_t pos; p = &panes[PaneFeeds]; if ((row = pane_row_get(p, p->pos))) f = row->data; pos = panes[PaneItems].pos; /* store numeric item position */ feeds_set(curfeed); /* close and reopen feed if possible */ urls_read(&urls, urlfile); feeds_load(feeds, nfeeds); urls_free(&urls); /* restore numeric item position */ pane_setpos(&panes[PaneItems], pos); updatesidebar(); updatetitle(); /* try to find the same feed in the pane */ if (f && (pos = feeds_row_get(p, f)) != -1) pane_setpos(p, pos); else pane_setpos(p, 0); } static void feed_open_selected(struct pane *p) { struct feed *f; struct row *row; if (!(row = pane_row_get(p, p->pos))) return; f = row->data; feeds_set(f); urls_read(&urls, urlfile); if (f->fp) feed_load(f, f->fp); urls_free(&urls); /* redraw row: counts could be changed */ updatesidebar(); updatetitle(); if (layout == LayoutMonocle) { selpane = PaneItems; updategeom(); } } static void feed_plumb_selected_item(struct pane *p, int field) { struct row *row; struct item *item; 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); } static void feed_pipe_selected_item(struct pane *p) { struct row *row; struct item *item; if (!(row = pane_row_get(p, p->pos))) return; item = row->data; markread(p, p->pos, p->pos, 1); pipeitem(pipercmd, item, -1, piperia); } static void feed_yank_selected_item(struct pane *p, int field) { struct row *row; struct item *item; if (!(row = pane_row_get(p, p->pos))) return; item = row->data; pipeitem(yankercmd, item, field, yankeria); } /* calculate optimal (default) size */ static int getsidebarsizedefault(void) { struct feed *feed; size_t i; int len, size; switch (layout) { case LayoutVertical: for (i = 0, size = 0; i < nfeeds; i++) { feed = &feeds[i]; len = snprintf(NULL, 0, " (%lu/%lu)", feed->totalnew, feed->total) + colw(feed->name); if (len > size) size = len; if (onlynew && feed->totalnew == 0) continue; } return MAX(MIN(win.width - 1, size), 0); case LayoutHorizontal: for (i = 0, size = 0; i < nfeeds; i++) { feed = &feeds[i]; if (onlynew && feed->totalnew == 0) continue; size++; } return MAX(MIN((win.height - 1) / 2, size), 1); } return 0; } static int getsidebarsize(void) { int size; if ((size = fixedsidebarsizes[layout]) < 0) size = getsidebarsizedefault(); return size; } static void adjustsidebarsize(int n) { int size; if ((size = fixedsidebarsizes[layout]) < 0) size = getsidebarsizedefault(); if (n > 0) { if ((layout == LayoutVertical && size + 1 < win.width) || (layout == LayoutHorizontal && size + 1 < win.height)) size++; } else if (n < 0) { if ((layout == LayoutVertical && size > 0) || (layout == LayoutHorizontal && size > 1)) size--; } if (size != fixedsidebarsizes[layout]) { fixedsidebarsizes[layout] = size; updategeom(); } } static void updatesidebar(void) { struct pane *p; struct row *row; struct feed *feed; size_t i, nrows; int oldvalue = 0, newvalue = 0; p = &panes[PaneFeeds]; if (!p->rows) p->rows = ecalloc(sizeof(p->rows[0]), nfeeds + 1); switch (layout) { case LayoutVertical: oldvalue = p->width; newvalue = getsidebarsize(); p->width = newvalue; break; case LayoutHorizontal: oldvalue = p->height; newvalue = getsidebarsize(); p->height = newvalue; break; } nrows = 0; for (i = 0; i < nfeeds; i++) { feed = &feeds[i]; row = &(p->rows[nrows]); row->bold = (feed->totalnew > 0); row->data = feed; if (onlynew && feed->totalnew == 0) continue; nrows++; } p->nrows = nrows; if (oldvalue != newvalue) updategeom(); else p->dirty = 1; if (!p->nrows) p->pos = 0; else if (p->pos >= p->nrows) p->pos = p->nrows - 1; } static void sighandler(int signo) { switch (signo) { 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; } } static void alldirty(void) { win.dirty = 1; panes[PaneFeeds].dirty = 1; panes[PaneItems].dirty = 1; scrollbars[PaneFeeds].dirty = 1; scrollbars[PaneItems].dirty = 1; linebar.dirty = 1; statusbar.dirty = 1; } static void draw(void) { struct row *row; struct item *item; size_t i; if (win.dirty) win.dirty = 0; for (i = 0; i < LEN(panes); i++) { pane_setfocus(&panes[i], i == selpane); pane_draw(&panes[i]); /* each pane has a scrollbar */ scrollbar_setfocus(&scrollbars[i], i == selpane); scrollbar_update(&scrollbars[i], panes[i].pos - (panes[i].pos % panes[i].height), panes[i].nrows, panes[i].height); scrollbar_draw(&scrollbars[i]); } linebar_draw(&linebar); /* if item selection text changed then update the status text */ if ((row = pane_row_get(&panes[PaneItems], panes[PaneItems].pos))) { item = row->data; statusbar_update(&statusbar, item->fields[FieldLink]); } else { statusbar_update(&statusbar, ""); } statusbar_draw(&statusbar); } static void mousereport(int button, int release, int keymask, int x, int y) { struct pane *p; size_t i; off_t pos; int changedpane, dblclick; if (!usemouse || release || button == -1) return; for (i = 0; i < LEN(panes); i++) { p = &panes[i]; if (p->hidden || !p->width || !p->height) continue; /* these button actions are done regardless of the position */ switch (button) { case 7: /* side-button: backward */ if (selpane == PaneFeeds) return; selpane = PaneFeeds; if (layout == LayoutMonocle) updategeom(); return; case 8: /* side-button: forward */ if (selpane == PaneItems) return; selpane = PaneItems; if (layout == LayoutMonocle) updategeom(); return; } /* check if mouse position is in pane or in its scrollbar */ if (!(x >= p->x && x < p->x + p->width + (!scrollbars[i].hidden) && y >= p->y && y < p->y + p->height)) continue; changedpane = (selpane != i); selpane = i; /* relative position on screen */ pos = y - p->y + p->pos - (p->pos % p->height); dblclick = (pos == p->pos); /* clicking the already selected row */ switch (button) { case 0: /* left-click */ if (!p->nrows || pos >= p->nrows) break; pane_setpos(p, pos); if (i == PaneFeeds) feed_open_selected(&panes[PaneFeeds]); else if (i == PaneItems && dblclick && !changedpane) feed_plumb_selected_item(&panes[PaneItems], FieldLink); break; case 2: /* right-click */ if (!p->nrows || pos >= p->nrows) break; pane_setpos(p, pos); if (i == PaneItems) feed_pipe_selected_item(&panes[PaneItems]); break; case 3: /* scroll up */ case 4: /* scroll down */ pane_scrollpage(p, button == 3 ? -1 : +1); break; } return; /* do not bubble events */ } } /* Custom formatter for feed row. */ static char * feed_row_format(struct pane *p, struct row *row) { /* static, reuse local buffers */ static char *bufw, *text; static size_t bufwsize, textsize; struct feed *feed; size_t needsize; char counts[128]; int len, w; feed = row->data; /* align counts to the right and pad the rest with spaces */ len = snprintf(counts, sizeof(counts), "(%lu/%lu)", feed->totalnew, feed->total); if (len > p->width) w = p->width; else w = p->width - len; needsize = (w + 1) * 4; if (needsize > bufwsize) { bufw = erealloc(bufw, needsize); bufwsize = needsize; } needsize = bufwsize + sizeof(counts) + 1; if (needsize > textsize) { text = erealloc(text, needsize); textsize = needsize; } if (utf8pad(bufw, bufwsize, feed->name, w, ' ') != -1) snprintf(text, textsize, "%s%s", bufw, counts); else text[0] = '\0'; return text; } static int feed_row_match(struct pane *p, struct row *row, const char *s) { struct feed *feed; feed = row->data; return (strcasestr(feed->name, s) != NULL); } static struct row * item_row_get(struct pane *p, off_t pos) { struct row *itemrow; struct item *item; struct feed *f; char *line = NULL; size_t linesize = 0; ssize_t linelen; itemrow = p->rows + pos; item = itemrow->data; f = curfeed; if (f && f->path && f->fp && !item->line) { if (fseek(f->fp, item->offset, SEEK_SET)) die("fseek: %s", f->path); if ((linelen = getline(&line, &linesize, f->fp)) <= 0) { if (ferror(f->fp)) die("getline: %s", f->path); free(line); return NULL; } if (line[linelen - 1] == '\n') line[--linelen] = '\0'; linetoitem(estrdup(line), item); free(line); itemrow->data = item; } return itemrow; } /* Custom formatter for item row. */ static char * item_row_format(struct pane *p, struct row *row) { /* static, reuse local buffers */ static char *text; static size_t textsize; struct item *item; struct tm tm; size_t needsize; item = row->data; needsize = strlen(item->fields[FieldTitle]) + 21; if (needsize > textsize) { text = erealloc(text, needsize); textsize = needsize; } if (item->timeok && localtime_r(&(item->timestamp), &tm)) { snprintf(text, textsize, "%c %04d-%02d-%02d %02d:%02d %s", item->fields[FieldEnclosure][0] ? '@' : ' ', tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, item->fields[FieldTitle]); } else { snprintf(text, textsize, "%c %s", item->fields[FieldEnclosure][0] ? '@' : ' ', item->fields[FieldTitle]); } return text; } static void markread(struct pane *p, off_t from, off_t to, int isread) { struct row *row; struct item *item; FILE *fp; off_t i; const char *cmd; int isnew = !isread, pid, status = -1, visstart; if (!urlfile || !p->nrows) return; cmd = isread ? markreadcmd : markunreadcmd; switch ((pid = fork())) { case -1: die("fork"); case 0: dup2(devnullfd, 1); /* stdout */ dup2(devnullfd, 2); /* stderr */ errno = 0; if (!(fp = popen(cmd, "w"))) die("popen: %s", cmd); for (i = from; i <= to && i < p->nrows; i++) { /* do not use pane_row_get(): no need for lazyload */ row = &(p->rows[i]); item = row->data; if (item->isnew != isnew) { fputs(item->matchnew, fp); putc('\n', fp); } } status = pclose(fp); status = WIFEXITED(status) ? WEXITSTATUS(status) : 127; _exit(status); default: /* 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 */ for (i = from; i <= to && i < p->nrows; i++) { row = &(p->rows[i]); item = row->data; if (item->isnew == isnew) continue; row->bold = item->isnew = isnew; curfeed->totalnew += isnew ? 1 : -1; /* draw if visible on screen */ if (i >= visstart && i < visstart + p->height) pane_row_draw(p, i, i == p->pos); } updatesidebar(); updatetitle(); } } static int urls_cmp(const void *v1, const void *v2) { return strcmp(*((char **)v1), *((char **)v2)); } static void urls_free(struct urls *urls) { while (urls->len > 0) { urls->len--; free(urls->items[urls->len]); } free(urls->items); urls->items = NULL; urls->len = 0; urls->cap = 0; } static int urls_hasmatch(struct urls *urls, const char *url) { return (urls->len && bsearch(&url, urls->items, urls->len, sizeof(char *), urls_cmp)); } static void urls_read(struct urls *urls, const char *urlfile) { FILE *fp; char *line = NULL; size_t linesiz = 0; ssize_t n; urls_free(urls); if (!urlfile) return; if (!(fp = fopen(urlfile, "rb"))) die("fopen: %s", urlfile); while ((n = getline(&line, &linesiz, fp)) > 0) { if (line[n - 1] == '\n') line[--n] = '\0'; if (urls->len + 1 >= urls->cap) { urls->cap = urls->cap ? urls->cap * 2 : 16; urls->items = erealloc(urls->items, urls->cap * sizeof(char *)); } urls->items[urls->len++] = estrdup(line); } if (ferror(fp)) die("getline: %s", urlfile); fclose(fp); free(line); if (urls->len > 0) qsort(urls->items, urls->len, sizeof(char *), urls_cmp); } int main(int argc, char *argv[]) { struct pane *p; struct feed *f; struct row *row; char *name, *tmp; char *search = NULL; /* search text */ int button, ch, fd, i, keymask, release, x, y; off_t pos; #ifdef __OpenBSD__ if (pledge("stdio rpath tty proc exec", NULL) == -1) die("pledge"); #endif setlocale(LC_CTYPE, ""); if ((tmp = getenv("SFEED_PLUMBER"))) plumbercmd = tmp; if ((tmp = getenv("SFEED_PIPER"))) pipercmd = tmp; if ((tmp = getenv("SFEED_YANKER"))) yankercmd = tmp; if ((tmp = getenv("SFEED_PLUMBER_INTERACTIVE"))) plumberia = !strcmp(tmp, "1"); if ((tmp = getenv("SFEED_PIPER_INTERACTIVE"))) piperia = !strcmp(tmp, "1"); if ((tmp = getenv("SFEED_YANKER_INTERACTIVE"))) yankeria = !strcmp(tmp, "1"); if ((tmp = getenv("SFEED_MARK_READ"))) markreadcmd = tmp; if ((tmp = getenv("SFEED_MARK_UNREAD"))) markunreadcmd = tmp; if ((tmp = getenv("SFEED_LAZYLOAD"))) lazyload = !strcmp(tmp, "1"); urlfile = getenv("SFEED_URL_FILE"); /* can be NULL */ cmdenv = getenv("SFEED_AUTOCMD"); /* can be NULL */ setlayout(argc <= 1 ? LayoutMonocle : LayoutVertical); selpane = layout == LayoutMonocle ? PaneItems : PaneFeeds; panes[PaneFeeds].row_format = feed_row_format; panes[PaneFeeds].row_match = feed_row_match; panes[PaneItems].row_format = item_row_format; if (lazyload) panes[PaneItems].row_get = item_row_get; feeds = ecalloc(argc, sizeof(struct feed)); if (argc == 1) { nfeeds = 1; f = &feeds[0]; f->name = "stdin"; if (!(f->fp = fdopen(0, "rb"))) die("fdopen"); } else { for (i = 1; i < argc; i++) { f = &feeds[i - 1]; f->path = argv[i]; name = ((name = strrchr(argv[i], '/'))) ? name + 1 : argv[i]; f->name = name; } nfeeds = argc - 1; } feeds_set(&feeds[0]); urls_read(&urls, urlfile); feeds_load(feeds, nfeeds); urls_free(&urls); if (!isatty(0)) { if ((fd = open("/dev/tty", O_RDONLY)) == -1) die("open: /dev/tty"); if (dup2(fd, 0) == -1) die("dup2(%d, 0): /dev/tty -> stdin", fd); close(fd); } if (argc == 1) feeds[0].fp = NULL; if ((devnullfd = open("/dev/null", O_WRONLY)) == -1) die("open: /dev/null"); init(); updatesidebar(); updategeom(); updatetitle(); draw(); while (1) { if ((ch = readch()) < 0) goto event; switch (ch) { case '\x1b': if ((ch = readch()) < 0) goto event; if (ch != '[' && ch != 'O') continue; /* unhandled */ if ((ch = readch()) < 0) goto event; switch (ch) { case 'M': /* mouse: X10 encoding */ if ((ch = readch()) < 0) goto event; button = ch - 32; if ((ch = readch()) < 0) goto event; x = ch - 32; if ((ch = readch()) < 0) goto event; y = ch - 32; keymask = button & (4 | 8 | 16); /* shift, meta, ctrl */ button &= ~keymask; /* unset key mask */ /* button numbers (0 - 2) encoded in lowest 2 bits * release does not indicate which button (so set to 0). * Handle extended buttons like scrollwheels * and side-buttons by each range. */ release = 0; if (button == 3) { button = -1; release = 1; } else if (button >= 128) { button -= 121; } else if (button >= 64) { button -= 61; } mousereport(button, release, keymask, x - 1, y - 1); break; case '<': /* mouse: SGR encoding */ for (button = 0; ; button *= 10, button += ch - '0') { if ((ch = readch()) < 0) goto event; else if (ch == ';') break; } for (x = 0; ; x *= 10, x += ch - '0') { if ((ch = readch()) < 0) goto event; else if (ch == ';') break; } for (y = 0; ; y *= 10, y += ch - '0') { if ((ch = readch()) < 0) goto event; else if (ch == 'm' || ch == 'M') break; /* release or press */ } release = ch == 'm'; keymask = button & (4 | 8 | 16); /* shift, meta, ctrl */ button &= ~keymask; /* unset key mask */ if (button >= 128) button -= 121; else if (button >= 64) button -= 61; mousereport(button, release, keymask, x - 1, y - 1); break; /* DEC/SUN: ESC O char, HP: ESC char or SCO: ESC [ char */ case 'A': goto keyup; /* arrow up */ case 'B': goto keydown; /* arrow down */ case 'C': goto keyright; /* arrow right */ case 'D': goto keyleft; /* arrow left */ case 'F': goto endpos; /* end */ case 'G': goto nextpage; /* page down */ case 'H': goto startpos; /* home */ case 'I': goto prevpage; /* page up */ default: if (!(ch >= '0' && ch <= '9')) break; 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: case 'k': pane_scrolln(&panes[selpane], -1); break; keydown: case 'j': pane_scrolln(&panes[selpane], +1); break; keyleft: case 'h': if (selpane == PaneFeeds) break; selpane = PaneFeeds; if (layout == LayoutMonocle) updategeom(); break; keyright: case 'l': if (selpane == PaneItems) break; selpane = PaneItems; if (layout == LayoutMonocle) updategeom(); break; case 'K': p = &panes[selpane]; if (!p->nrows) break; for (pos = p->pos - 1; pos >= 0; pos--) { if ((row = pane_row_get(p, pos)) && row->bold) { pane_setpos(p, pos); break; } } break; case 'J': p = &panes[selpane]; if (!p->nrows) break; for (pos = p->pos + 1; pos < p->nrows; pos++) { if ((row = pane_row_get(p, pos)) && row->bold) { pane_setpos(p, pos); break; } } break; case '\t': selpane = selpane == PaneFeeds ? PaneItems : PaneFeeds; if (layout == LayoutMonocle) updategeom(); break; startpos: case 'g': pane_setpos(&panes[selpane], 0); break; endpos: case 'G': p = &panes[selpane]; if (p->nrows) pane_setpos(p, p->nrows - 1); break; prevpage: case 2: /* ^B */ pane_scrollpage(&panes[selpane], -1); break; nextpage: case ' ': case 6: /* ^F */ pane_scrollpage(&panes[selpane], +1); break; case '[': case ']': pane_scrolln(&panes[PaneFeeds], ch == '[' ? -1 : +1); feed_open_selected(&panes[PaneFeeds]); break; case '/': /* new search (forward) */ case '?': /* new search (backward) */ case 'n': /* search again (forward) */ case 'N': /* search again (backward) */ p = &panes[selpane]; /* prompt for new input */ if (ch == '?' || ch == '/') { tmp = ch == '?' ? "backward" : "forward"; free(search); search = uiprompt(statusbar.x, statusbar.y, "Search (%s):", tmp); statusbar.dirty = 1; } if (!search || !p->nrows) break; if (ch == '/' || ch == 'n') { /* forward */ for (pos = p->pos + 1; pos < p->nrows; pos++) { if (pane_row_match(p, pane_row_get(p, pos), search)) { pane_setpos(p, pos); break; } } } else { /* backward */ for (pos = p->pos - 1; pos >= 0; pos--) { if (pane_row_match(p, pane_row_get(p, pos), search)) { pane_setpos(p, pos); break; } } } break; case 12: /* ^L, redraw */ alldirty(); break; case 'R': /* reload all files */ feeds_reloadall(); break; case 'a': /* attachment */ case 'e': /* enclosure */ case '@': if (selpane == PaneItems) feed_plumb_selected_item(&panes[selpane], FieldEnclosure); break; case 'm': /* toggle mouse mode */ usemouse = !usemouse; mousemode(usemouse); break; case '<': /* decrease fixed sidebar width */ case '>': /* increase fixed sidebar width */ adjustsidebarsize(ch == '<' ? -1 : +1); break; case '=': /* reset fixed sidebar to automatic size */ fixedsidebarsizes[layout] = -1; updategeom(); break; case 't': /* toggle showing only new in sidebar */ p = &panes[PaneFeeds]; if ((row = pane_row_get(p, p->pos))) f = row->data; else f = NULL; onlynew = !onlynew; updatesidebar(); /* try to find the same feed in the pane */ if (f && f->totalnew && (pos = feeds_row_get(p, f)) != -1) pane_setpos(p, pos); else pane_setpos(p, 0); break; case 'o': /* feeds: load, items: plumb URL */ case '\n': if (selpane == PaneFeeds && panes[selpane].nrows) feed_open_selected(&panes[selpane]); else if (selpane == PaneItems && panes[selpane].nrows) feed_plumb_selected_item(&panes[selpane], FieldLink); break; case 'c': /* items: pipe TSV line to program */ case 'p': case '|': if (selpane == PaneItems) feed_pipe_selected_item(&panes[selpane]); break; case 'y': /* yank: pipe TSV field to yank URL to clipboard */ case 'E': /* yank: pipe TSV field to yank enclosure to clipboard */ if (selpane == PaneItems) feed_yank_selected_item(&panes[selpane], ch == 'y' ? FieldLink : FieldEnclosure); break; case 'f': /* mark all read */ case 'F': /* mark all unread */ if (panes[PaneItems].nrows) { p = &panes[PaneItems]; markread(p, 0, p->nrows - 1, ch == 'f'); } break; case 'r': /* mark item as read */ case 'u': /* mark item as unread */ 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 */ setlayout(layout == LayoutMonocle ? prevlayout : LayoutMonocle); updategeom(); break; case '1': /* vertical layout */ case '2': /* horizontal layout */ case '3': /* monocle layout */ setlayout(ch - '1'); updategeom(); break; case 4: /* EOT */ case 'q': goto end; } event: if (ch == EOF) goto end; else if (ch == -3 && !state_sigchld && !state_sighup && !state_sigint && !state_sigterm && !state_sigwinch) continue; /* just a time-out, nothing to do */ /* 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 + SIGINT); } if (state_sighup) { state_sighup = 0; feeds_reloadall(); } if (state_sigwinch) { state_sigwinch = 0; resizewin(); updategeom(); } draw(); } end: cleanup(); return 0; } sfeed-2.2/sfeed_frames.1000066400000000000000000000027411476411343700151660ustar00rootroot00000000000000.Dd July 31, 2021 .Dt SFEED_FRAMES 1 .Os .Sh NAME .Nm sfeed_frames .Nd format feed data to HTML with frames .Sh SYNOPSIS .Nm .Op Ar .Sh DESCRIPTION .Nm formats feed data (TSV) from .Xr sfeed 1 from stdin or for each .Ar file to HTML. It writes HTML files for the frameset to the current directory. If no .Ar file arguments are specified and so the data is read from stdin then the menu.html file is not 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 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 .Bl -tag -width 13n .It index.html The main HTML file referencing the files for the frames: items.html and menu.html. .It items.html The HTML file of the items frame which contains all the item links to the feeds. .It menu.html 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 .Bd -literal sfeed_frames ~/.sfeed/feeds/* .Ed .Sh SEE ALSO .Xr sfeed 1 , .Xr sfeed_html 1 , .Xr sfeed_plain 1 , .Xr sfeed 5 .Sh AUTHORS .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org sfeed-2.2/sfeed_frames.c000066400000000000000000000122021476411343700152410ustar00rootroot00000000000000#include #include #include #include #include "util.h" static struct feed *feeds; static char *line; static size_t linesize; static time_t comparetime; static unsigned long totalnew, total; static void printfeed(FILE *fpitems, FILE *fpin, struct feed *f) { char *fields[FieldLast]; ssize_t linelen; unsigned int isnew; struct tm rtm, *tm; time_t parsedtime; /* menu if not unnamed */ if (f->name[0]) { fputs("

name, fpitems); fputs("\">name, fpitems); fputs("\">", fpitems); xmlencode(f->name, fpitems); fputs("

\n", fpitems); } fputs("
\n", fpitems);

	while ((linelen = getline(&line, &linesize, fpin)) > 0 &&
	       !ferror(fpitems)) {
		if (line[linelen - 1] == '\n')
			line[--linelen] = '\0';
		parseline(line, fields);

		parsedtime = 0;
		if (!strtotime(fields[FieldUnixTimestamp], &parsedtime) &&
		    (tm = localtime_r(&parsedtime, &rtm))) {
			isnew = (parsedtime >= comparetime) ? 1 : 0;
			totalnew += isnew;
			f->totalnew += isnew;
			fprintf(fpitems, "%04d-%02d-%02d %02d:%02d ",
			        tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
			        tm->tm_hour, tm->tm_min);
		} else {
			isnew = 0;
			fputs("                 ", fpitems);
		}
		f->total++;
		total++;

		if (fields[FieldLink][0]) {
			fputs("", fpitems);
		}
		if (isnew)
			fputs("", fpitems);
		xmlencode(fields[FieldTitle], fpitems);
		if (isnew)
			fputs("", fpitems);
		if (fields[FieldLink][0])
			fputs("", fpitems);
		fputs("\n", fpitems);
	}
	fputs("
\n", fpitems); } int main(int argc, char *argv[]) { FILE *fpindex, *fpitems, *fpmenu = NULL, *fp; char *name; int i, showsidebar = (argc > 1); struct feed *f; if (pledge("stdio rpath wpath cpath", NULL) == -1) err(1, "pledge"); if (!(feeds = calloc(argc, sizeof(struct feed)))) err(1, "calloc"); if ((comparetime = getcomparetime()) == (time_t)-1) errx(1, "getcomparetime"); /* write main index page */ if (!(fpindex = fopen("index.html", "wb"))) err(1, "fopen: index.html"); if (!(fpitems = fopen("items.html", "wb"))) err(1, "fopen: items.html"); if (showsidebar && !(fpmenu = fopen("menu.html", "wb"))) err(1, "fopen: menu.html"); if (pledge(argc == 1 ? "stdio" : "stdio rpath", NULL) == -1) err(1, "pledge"); fputs("\n" "\n" "\t\n" "\t\n" "\t\n" "\t\n" "\n" "
", fpitems); if (argc == 1) { feeds[0].name = ""; printfeed(fpitems, stdin, &feeds[0]); checkfileerror(stdin, "", 'r'); } else { for (i = 1; i < argc; i++) { name = ((name = strrchr(argv[i], '/'))) ? name + 1 : argv[i]; feeds[i - 1].name = name; if (!(fp = fopen(argv[i], "r"))) err(1, "fopen: %s", argv[i]); printfeed(fpitems, fp, &feeds[i - 1]); checkfileerror(fp, argv[i], 'r'); checkfileerror(fpitems, "items.html", 'w'); fclose(fp); } } fputs("
\n\n", fpitems); /* div items */ if (showsidebar) { fputs("\n" "\n" "\n" "\t\n" "\t\n" "\n" "\n\n", fpmenu); } fputs("\n\n\n" "\t\n" "\t\n" "\t(", fpindex); fprintf(fpindex, "%lu/%lu", totalnew, total); fputs(") - Newsfeed\n\t\n" "\n", fpindex); if (showsidebar) { fputs("\n" "\t\n", fpindex); } else { fputs("\n", fpindex); } fputs( "\t\n" "\n" "\n", fpindex); checkfileerror(fpindex, "index.html", 'w'); checkfileerror(fpitems, "items.html", 'w'); fclose(fpindex); fclose(fpitems); if (fpmenu) { checkfileerror(fpmenu, "menu.html", 'w'); fclose(fpmenu); } return 0; } sfeed-2.2/sfeed_gopher.1000066400000000000000000000035501476411343700151740ustar00rootroot00000000000000.Dd May 14, 2022 .Dt SFEED_GOPHER 1 .Os .Sh NAME .Nm sfeed_gopher .Nd format feed data to Gopher files .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 in the raw Gopher output format. .Pp 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. .Nm creates an index file named "index" and for each feed it creates a file with the same name as the basename of the feed .Ar file . .Pp If no .Ar file arguments are specified and so the data is read from stdin then the data is 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. 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 This environment variable can be used as the prefix for each path in the index file. The default is "/". .It Ev SFEED_GOPHER_HOST This environment variable can be used as the Gopher Host field. 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/" \e sfeed_gopher ~/.sfeed/feeds/* .Ed .Sh SEE ALSO .Xr sfeed 1 , .Xr sfeed 5 .Sh STANDARDS .Rs .%D March 1993 .%R RFC 1436 .%T The Internet Gopher Protocol .Re .Sh AUTHORS .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org .Sh CAVEATS The common-used 'h' and 'i' types are used in this program. These types are an extension to Gopher. sfeed-2.2/sfeed_gopher.c000066400000000000000000000104641476411343700152600ustar00rootroot00000000000000#include #include #include #include #include "util.h" 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 */ static void gophertext(FILE *fp, const char *s) { for (; *s; s++) { switch (*s) { case '\r': /* ignore CR */ case '\n': /* ignore LF */ break; case '\t': fputs(" ", fp); break; default: putc(*s, fp); break; } } } static void printfeed(FILE *fpitems, FILE *fpin, struct feed *f) { struct uri u; char *fields[FieldLast]; char *itemhost, *itemport, *itempath, *itemquery, *itemfragment; ssize_t linelen; unsigned int isnew; struct tm rtm, *tm; time_t parsedtime; int itemtype; if (f->name[0]) { fprintf(fpitems, "i%s\t\t%s\t%s\r\n", f->name, host, port); fprintf(fpitems, "i\t\t%s\t%s\r\n", host, port); } while ((linelen = getline(&line, &linesize, fpin)) > 0 && !ferror(fpitems)) { if (line[linelen - 1] == '\n') line[--linelen] = '\0'; parseline(line, fields); itemhost = host; itemport = port; itemtype = 'i'; itempath = fields[FieldLink]; itemquery = ""; itemfragment = ""; if (fields[FieldLink][0]) { itemtype = 'h'; /* 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; itemport = u.port[0] ? u.port : "70"; itemtype = '1'; itempath = u.path; itemquery = u.query; itemfragment = u.fragment; if (itempath[0] == '/') { itempath++; if (*itempath) { itemtype = *itempath; itempath++; } } } } parsedtime = 0; if (!strtotime(fields[FieldUnixTimestamp], &parsedtime) && (tm = localtime_r(&parsedtime, &rtm))) { isnew = (parsedtime >= comparetime) ? 1 : 0; f->totalnew += isnew; fprintf(fpitems, "%c%c %04d-%02d-%02d %02d:%02d ", itemtype, isnew ? 'N' : ' ', tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min); } else { fprintf(fpitems, "%c ", itemtype); } f->total++; gophertext(fpitems, fields[FieldTitle]); fputs("\t", fpitems); if (itemtype == 'h' && fields[FieldLink] == itempath) fputs("URL:", fpitems); gophertext(fpitems, itempath); if (itemquery[0]) { fputs("?", fpitems); gophertext(fpitems, itemquery); } if (itemfragment[0]) { fputs("#", fpitems); gophertext(fpitems, itemfragment); } fprintf(fpitems, "\t%s\t%s\r\n", itemhost, itemport); } fputs(".\r\n", fpitems); } int main(int argc, char *argv[]) { struct feed f = { 0 }; FILE *fpitems, *fpindex, *fp; char *name, *p; int i; if (argc == 1) { if (pledge("stdio", NULL) == -1) err(1, "pledge"); } else { if (unveil("/", "r") == -1) err(1, "unveil: /"); if (unveil(".", "rwc") == -1) err(1, "unveil: ."); if (pledge("stdio rpath wpath cpath", NULL) == -1) err(1, "pledge"); } if ((comparetime = getcomparetime()) == (time_t)-1) errx(1, "getcomparetime"); if ((p = getenv("SFEED_GOPHER_HOST"))) host = p; if ((p = getenv("SFEED_GOPHER_PORT"))) port = p; if (argc == 1) { f.name = ""; printfeed(stdout, stdin, &f); checkfileerror(stdin, "", 'r'); checkfileerror(stdout, "", 'w'); } else { if ((p = getenv("SFEED_GOPHER_PATH"))) prefixpath = p; /* write main index page */ if (!(fpindex = fopen("index", "wb"))) err(1, "fopen: index"); for (i = 1; i < argc; i++) { memset(&f, 0, sizeof(f)); name = ((name = strrchr(argv[i], '/'))) ? name + 1 : argv[i]; f.name = name; if (!(fp = fopen(argv[i], "r"))) err(1, "fopen: %s", argv[i]); if (!(fpitems = fopen(name, "wb"))) err(1, "fopen"); printfeed(fpitems, fp, &f); checkfileerror(fp, argv[i], 'r'); checkfileerror(fpitems, name, 'w'); fclose(fp); fclose(fpitems); /* append directory item to index */ fputs("1", fpindex); gophertext(fpindex, name); fprintf(fpindex, " (%lu/%lu)\t", f.totalnew, f.total); gophertext(fpindex, prefixpath); gophertext(fpindex, name); fprintf(fpindex, "\t%s\t%s\r\n", host, port); } fputs(".\r\n", fpindex); checkfileerror(fpindex, "index", 'w'); fclose(fpindex); } return 0; } sfeed-2.2/sfeed_html.1000066400000000000000000000022231476411343700146500ustar00rootroot00000000000000.Dd July 31, 2021 .Dt SFEED_HTML 1 .Os .Sh NAME .Nm sfeed_html .Nd format feed data to HTML .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 in HTML. 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 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 .Bd -literal sfeed_html ~/.sfeed/feeds/* .Ed .Sh SEE ALSO .Xr sfeed 1 , .Xr sfeed_frames 1 , .Xr sfeed_plain 1 , .Xr sfeed 5 .Sh AUTHORS .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org sfeed-2.2/sfeed_html.c000066400000000000000000000072651476411343700147450ustar00rootroot00000000000000#include #include #include #include #include "util.h" static struct feed *feeds; static int showsidebar; static char *line; static size_t linesize; static unsigned long totalnew, total; static time_t comparetime; static void printfeed(FILE *fp, struct feed *f) { char *fields[FieldLast]; struct tm rtm, *tm; time_t parsedtime; unsigned int isnew; ssize_t linelen; if (f->name[0]) { fputs("

name, stdout); fputs("\">name, stdout); fputs("\">", stdout); xmlencode(f->name, stdout); fputs("

\n", stdout); } fputs("
\n", stdout);

	while ((linelen = getline(&line, &linesize, fp)) > 0 &&
	       !ferror(stdout)) {
		if (line[linelen - 1] == '\n')
			line[--linelen] = '\0';
		parseline(line, fields);

		parsedtime = 0;
		if (!strtotime(fields[FieldUnixTimestamp], &parsedtime) &&
		    (tm = localtime_r(&parsedtime, &rtm))) {
			isnew = (parsedtime >= comparetime) ? 1 : 0;
			totalnew += isnew;
			f->totalnew += isnew;

			fprintf(stdout, "%04d-%02d-%02d %02d:%02d ",
			        tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
			        tm->tm_hour, tm->tm_min);
		} else {
			isnew = 0;
			fputs("                 ", stdout);
		}
		f->total++;
		total++;

		if (fields[FieldLink][0]) {
			fputs("", stdout);
		}
		if (isnew)
			fputs("", stdout);
		xmlencode(fields[FieldTitle], stdout);
		if (isnew)
			fputs("", stdout);
		if (fields[FieldLink][0])
			fputs("", stdout);
		fputs("\n", stdout);
	}
	fputs("
\n", stdout); } int main(int argc, char *argv[]) { struct feed *f; char *name; FILE *fp; int i; if (pledge(argc == 1 ? "stdio" : "stdio rpath", NULL) == -1) err(1, "pledge"); if (!(feeds = calloc(argc, sizeof(struct feed)))) err(1, "calloc"); if ((comparetime = getcomparetime()) == (time_t)-1) errx(1, "getcomparetime"); fputs("\n" "\n" "\t\n" "\t\n" "\t\t\n" "\t\t\n" "\t\n" "\t\n", stdout); showsidebar = (argc > 1); if (showsidebar) fputs("\t\t
\n", stdout); else fputs("\t\t
\n", stdout); if (argc == 1) { feeds[0].name = ""; printfeed(stdin, &feeds[0]); checkfileerror(stdin, "", 'r'); } else { for (i = 1; i < argc; i++) { name = ((name = strrchr(argv[i], '/'))) ? name + 1 : argv[i]; feeds[i - 1].name = name; if (!(fp = fopen(argv[i], "r"))) err(1, "fopen: %s", argv[i]); printfeed(fp, &feeds[i - 1]); checkfileerror(fp, argv[i], 'r'); checkfileerror(stdout, "", 'w'); fclose(fp); } } fputs("
\n", stdout); /* div items */ if (showsidebar) { fputs("\t\n", stdout); } fprintf(stdout, "\t\n\t(%lu/%lu) - Newsfeed\n\n", totalnew, total); checkfileerror(stdout, "", 'w'); return 0; } sfeed-2.2/sfeed_json.1000066400000000000000000000016311476411343700146570ustar00rootroot00000000000000.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 sfeed-2.2/sfeed_json.c000066400000000000000000000072071476411343700147460ustar00rootroot00000000000000#include #include #include #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, "", '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, "", 'w'); fclose(fp); } } fputs("]\n}\n", stdout); checkfileerror(stdout, "", 'w'); return 0; } sfeed-2.2/sfeed_markread000077500000000000000000000013621476411343700153410ustar00rootroot00000000000000#!/bin/sh # Mark items as read/unread: the input is the read / unread URL per line. usage() { printf "usage: %s [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 [ -z "${urlfile}" ]; then usage fi case "$1" in read) cat >> "${urlfile}" ;; unread) tmp="$(mktemp)" || exit 1 trap "rm -f ${tmp}" EXIT [ -f "${urlfile}" ] || touch "${urlfile}" 2>/dev/null LC_ALL=C awk -F '\t' ' { FILENR += (FNR == 1) } FILENR == 1 { urls[$0] = 1 } FILENR == 2 { if (!urls[$0]) { print $0 } } END { exit(FILENR != 2) }' \ "-" "${urlfile}" > "${tmp}" && \ cp "${tmp}" "${urlfile}" ;; *) usage ;; esac sfeed-2.2/sfeed_markread.1000066400000000000000000000020621476411343700154730ustar00rootroot00000000000000.Dd October 27, 2024 .Dt SFEED_MARKREAD 1 .Os .Sh NAME .Nm sfeed_markread .Nd mark items as read/unread .Sh SYNOPSIS .Nm .Ar read | Ar unread .Op Ar urlfile .Sh DESCRIPTION .Nm reads a plain-text list of URLs from stdin. The file format for the list of URLs is one URL per line. .Nm will write to the file specified as .Ar urlfile or with the environment variable .Ev SFEED_URL_FILE . The .Nm script can be used by .Xr sfeed_curses 1 to mark items as read and unread. .Sh ENVIRONMENT VARIABLES .Bl -tag -width Ds .It Ev SFEED_URL_FILE This variable can be set to use as the path to the file containing a plain-text list of read URLs. .El .Sh EXIT STATUS .Ex -std .Sh EXAMPLES Example: .Bd -literal -offset 4n export SFEED_URL_FILE="$HOME/.sfeed/urls" echo 'https://codemadness.org/sfeed.html' | sfeed_markread read .Ed .Pp or pass the .Ar urlfile as a parameter: .Bd -literal -offset 4n echo 'https://codemadness.org/sfeed.html' | sfeed_markread read ~/.sfeed/urls .Ed .Sh SEE ALSO .Xr awk 1 , .Xr sfeed_curses 1 .Sh AUTHORS .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org sfeed-2.2/sfeed_mbox.1000066400000000000000000000026551476411343700146620ustar00rootroot00000000000000.Dd October 27, 2024 .Dt SFEED_MBOX 1 .Os .Sh NAME .Nm sfeed_mbox .Nd format feed data to mboxrd .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 in the mboxrd format. 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. The mbox data can be further processed by tools like .Xr procmail 1 or .Xr fdm 1 for example. See the README file for some examples. .Sh CUSTOM HEADERS To make further filtering simpler some custom headers are set: .Bl -tag -width Ds .It X-Feedname The feed name, this is the basename of the feed .Ar file . .El .Sh ENVIRONMENT VARIABLES .Bl -tag -width Ds .It Ev SFEED_MBOX_CONTENT Include the content. This can be insecure for some of the mail clients that interpret HTML code in an unsafe way. By default this is set to "0". .El .Sh EXIT STATUS .Ex -std .Sh EXAMPLES Example: .Bd -literal -offset 4n sfeed_mbox ~/.sfeed/feeds/* .Ed .Pp Below is an example to include the content. This can be insecure for some of the mail clients that interpret HTML code in an unsafe way: .Bd -literal -offset 4n SFEED_MBOX_CONTENT=1 sfeed_mbox ~/.sfeed/feeds/* .Ed .Sh SEE ALSO .Xr fdm 1 , .Xr procmail 1 , .Xr sfeed 1 , .Xr sfeed 5 .Sh AUTHORS .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org sfeed-2.2/sfeed_mbox.c000066400000000000000000000114321476411343700147350ustar00rootroot00000000000000#include #include #include #include #include #include "util.h" static char *line; static size_t linesize; static char host[256], *user, dtimebuf[32], mtimebuf[32]; static int usecontent = 0; /* env variable: $SFEED_MBOX_CONTENT */ static unsigned long long djb2(unsigned char *s, unsigned long long hash) { int c; while ((c = *s++)) hash = ((hash << 5) + hash) + c; /* hash * 33 + c */ return hash; } /* Unescape / decode fields printed by string_print_encoded() * "\\" 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++) putc('>', fp); /* escape "From ", mboxrd-style. */ if (!strncmp(s, "From ", 5)) putc('>', fp); for (; *s; s++) { switch (*s) { case '\\': if (*(s + 1) == '\0') break; s++; switch (*s) { case 'n': putc('\n', fp); s++; goto escapefrom; case '\\': putc('\\', fp); break; case 't': putc('\t', fp); break; } break; default: putc(*s, fp); break; } } } static void printfeed(FILE *fp, const char *feedname) { char *fields[FieldLast], timebuf[32]; struct tm parsedtm, *tm; time_t parsedtime; unsigned long long hash; ssize_t linelen; int ishtml; while ((linelen = getline(&line, &linesize, fp)) > 0 && !ferror(stdout)) { if (line[linelen - 1] == '\n') line[--linelen] = '\0'; hash = djb2((unsigned char *)line, 5381ULL); parseline(line, fields); /* mbox + mail header */ printf("From MAILER-DAEMON %s\n", mtimebuf); parsedtime = 0; if (!strtotime(fields[FieldUnixTimestamp], &parsedtime) && (tm = gmtime_r(&parsedtime, &parsedtm)) && strftime(timebuf, sizeof(timebuf), "%a, %d %b %Y %H:%M:%S +0000", tm)) { printf("Date: %s\n", timebuf); } else { printf("Date: %s\n", dtimebuf); /* invalid/missing: use current time */ } printf("From: %s \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", fields[FieldUnixTimestamp], fields[FieldUnixTimestamp][0] ? "." : "", hash, feedname); ishtml = usecontent && !strcmp(fields[FieldContentType], "html"); if (ishtml) fputs("Content-Type: text/html; charset=\"utf-8\"\n", stdout); else fputs("Content-Type: text/plain; charset=\"utf-8\"\n", stdout); fputs("Content-Transfer-Encoding: binary\n", stdout); printf("X-Feedname: %s\n", feedname); fputs("\n", stdout); if (ishtml) { fputs("

\n", stdout); if (fields[FieldLink][0]) { fputs("Link: ", stdout); xmlencode(fields[FieldLink], stdout); fputs("
\n", stdout); } if (fields[FieldEnclosure][0]) { fputs("Enclosure: ", stdout); xmlencode(fields[FieldEnclosure], stdout); fputs("
\n", stdout); } fputs("

\n", stdout); } else { if (fields[FieldLink][0]) printf("Link: %s\n", fields[FieldLink]); if (fields[FieldEnclosure][0]) printf("Enclosure: %s\n", fields[FieldEnclosure]); } if (usecontent) { fputs("\n", stdout); if (ishtml && fields[FieldLink][0]) { fputs("\n", stdout); } printcontent(fields[FieldContent], stdout); } fputs("\n\n", stdout); } } int main(int argc, char *argv[]) { struct tm tmnow; time_t now; FILE *fp; char *name, *tmp; int i; if (pledge(argc == 1 ? "stdio" : "stdio rpath", NULL) == -1) err(1, "pledge"); if ((tmp = getenv("SFEED_MBOX_CONTENT"))) usecontent = !strcmp(tmp, "1"); if (!(user = getenv("USER"))) user = "you"; if (gethostname(host, sizeof(host)) == -1) err(1, "gethostname"); 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)) errx(1, "strftime: can't format current time"); if (!strftime(dtimebuf, sizeof(dtimebuf), "%a, %d %b %Y %H:%M:%S +0000", &tmnow)) errx(1, "strftime: can't format current time"); if (argc == 1) { printfeed(stdin, ""); checkfileerror(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, "", 'w'); fclose(fp); } } checkfileerror(stdout, "", 'w'); return 0; } sfeed-2.2/sfeed_opml_export000077500000000000000000000025151476411343700161240ustar00rootroot00000000000000#!/bin/sh # load config (evaluate shellscript). # loadconfig(configfile) loadconfig() { # allow to specify config via argv[1]. if [ "$1" != "" ]; then # get absolute path of config file required for including. config="$1" configpath=$(readlink -f "${config}" 2>/dev/null) else # default config location. config="$HOME/.sfeed/sfeedrc" configpath="${config}" fi # config is loaded here to be able to override $sfeedpath or functions. if [ -r "${configpath}" ] && [ -f "${configpath}" ]; then . "${configpath}" else 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 } # override feed function to output OPML XML. # feed(name, feedurl, [basesiteurl], [encoding]) feed() { # uses the characters 0x1f and 0x1e as a separator. printf '%s\037%s\036' "$1" "$2" } # load config file. loadconfig "$1" cat < OPML export ! feeds | LC_ALL=C awk ' BEGIN { FS = "\x1f"; RS = "\x1e"; } { gsub("&", "\\&"); gsub("\"", "\\""); gsub("'"'"'", "\\'"); gsub("<", "\\<"); gsub(">", "\\>"); print "\t"; }' cat < ! sfeed-2.2/sfeed_opml_export.1000066400000000000000000000011621476411343700162550ustar00rootroot00000000000000.Dd January 3, 2021 .Dt SFEED_OPML_EXPORT 1 .Os .Sh NAME .Nm sfeed_opml_export .Nd export feeds in a sfeedrc file to OPML data .Sh SYNOPSIS .Nm .Op Ar sfeedrc .Sh DESCRIPTION .Nm reads the specified .Ar sfeedrc config file and output OPML XML data to stdout. .Sh OPTIONS .Bl -tag -width Ds .It sfeedrc Default: .Pa $HOME/.sfeed/sfeedrc see the .Xr sfeed_update 1 .Sx FILES READ section for more information. .El .Sh EXIT STATUS .Ex -std .Sh EXAMPLES .Bd -literal sfeed_opml_export ~/.sfeed/sfeedrc > opmlfile.xml .Ed .Sh SEE ALSO .Xr sfeed_update 1 , .Xr sfeedrc 5 .Sh AUTHORS .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org sfeed-2.2/sfeed_opml_import.1000066400000000000000000000007351476411343700162530ustar00rootroot00000000000000.Dd January 15, 2021 .Dt SFEED_OPML_IMPORT 1 .Os .Sh NAME .Nm sfeed_opml_import .Nd generate a sfeedrc config file from an OPML subscription list .Sh SYNOPSIS .Nm .Sh DESCRIPTION .Nm reads the OPML XML data from stdin and writes the config file text to stdout. .Sh EXIT STATUS .Ex -std .Sh EXAMPLES .Bd -literal sfeed_opml_import < opmlfile.xml > ~/.sfeed/sfeedrc .Ed .Sh SEE ALSO .Xr sfeed_update 1 , .Xr sfeedrc 5 .Sh AUTHORS .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org sfeed-2.2/sfeed_opml_import.c000066400000000000000000000042271476411343700163350ustar00rootroot00000000000000#include #include #include "util.h" #include "xml.h" static XMLParser parser; /* XML parser state */ static char text[256], title[256], xmlurl[2048]; static void printsafe(const char *s) { for (; *s; s++) { if (ISCNTRL((unsigned char)*s)) continue; else if (*s == '\\') fputs("\\\\", stdout); else if (*s == '\'') fputs("'\\''", stdout); else putchar(*s); } } static void xmltagstart(XMLParser *p, const char *t, size_t tl) { text[0] = title[0] = xmlurl[0] = '\0'; } static void xmltagend(XMLParser *p, const char *t, size_t tl, int isshort) { if (strcasecmp(t, "outline")) return; if (xmlurl[0]) { fputs("\tfeed '", stdout); /* prefer title over text attribute */ if (title[0]) printsafe(title); else if (text[0]) printsafe(text); else fputs("unnamed", stdout); fputs("' '", stdout); printsafe(xmlurl); fputs("'\n", stdout); } text[0] = title[0] = xmlurl[0] = '\0'; } static void xmlattr(XMLParser *p, const char *t, size_t tl, const char *n, size_t nl, const char *v, size_t vl) { if (strcasecmp(t, "outline")) return; if (!strcasecmp(n, "text")) strlcat(text, v, sizeof(text)); else if (!strcasecmp(n, "title")) strlcat(title, v, sizeof(title)); else if (!strcasecmp(n, "xmlurl")) strlcat(xmlurl, v, sizeof(xmlurl)); } 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[8]; int len; if ((len = xml_entitytostr(v, buf, sizeof(buf))) > 0) xmlattr(p, t, tl, n, nl, buf, len); else xmlattr(p, t, tl, n, nl, v, vl); } int main(void) { if (pledge("stdio", NULL) == -1) err(1, "pledge"); parser.xmlattr = xmlattr; parser.xmlattrentity = xmlattrentity; parser.xmltagstart = xmltagstart; parser.xmltagend = xmltagend; fputs( "#sfeedpath=\"$HOME/.sfeed/feeds\"\n" "\n" "# list of feeds to fetch:\n" "feeds() {\n" " # feed [basesiteurl] [encoding]\n", stdout); /* NOTE: GETNEXT is defined in xml.h for inline optimization */ xml_parse(&parser); fputs("}\n", stdout); checkfileerror(stdin, "", 'r'); checkfileerror(stdout, "", 'w'); return 0; } sfeed-2.2/sfeed_plain.1000066400000000000000000000026771476411343700150240ustar00rootroot00000000000000.Dd May 14, 2022 .Dt SFEED_PLAIN 1 .Os .Sh NAME .Nm sfeed_plain .Nd format feed data to a plain-text list .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 a plain-text list. 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 Items with a timestamp from the last day compared to the system time at the time of formatting are marked as new. This value might be overridden through environment variables. Items are marked as new with the prefix "N" at the start of the line. .Pp .Nm aligns the output. It shows a maximum of 70 column-wide characters for the title and outputs an ellipsis symbol if the title is longer and truncated. Make sure the environment variable .Ev LC_CTYPE is set to a UTF-8 locale, so it can determine the proper column-width 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 .Bd -literal curl -s 'https://codemadness.org/atom.xml' | sfeed | sfeed_plain .Ed .Sh SEE ALSO .Xr sfeed 1 , .Xr sfeed_html 1 , .Xr sfeed 5 .Sh AUTHORS .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org sfeed-2.2/sfeed_plain.c000066400000000000000000000035021476411343700150720ustar00rootroot00000000000000#include #include #include #include #include "util.h" static time_t comparetime; static char *line; static size_t linesize; static void printfeed(FILE *fp, const char *feedname) { char *fields[FieldLast]; struct tm rtm, *tm; time_t parsedtime; ssize_t linelen; while ((linelen = getline(&line, &linesize, fp)) > 0 && !ferror(stdout)) { if (line[linelen - 1] == '\n') line[--linelen] = '\0'; parseline(line, fields); parsedtime = 0; if (!strtotime(fields[FieldUnixTimestamp], &parsedtime) && (tm = localtime_r(&parsedtime, &rtm))) { if (parsedtime >= comparetime) fputs("N ", stdout); else fputs(" ", stdout); fprintf(stdout, "%04d-%02d-%02d %02d:%02d ", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min); } else { fputs(" ", stdout); } if (feedname[0]) { printutf8pad(stdout, feedname, 15, ' '); fputs(" ", stdout); } printutf8pad(stdout, fields[FieldTitle], 70, ' '); printf(" %s\n", fields[FieldLink]); } } int main(int argc, char *argv[]) { FILE *fp; char *name; int i; if (pledge("stdio rpath", NULL) == -1) err(1, "pledge"); setlocale(LC_CTYPE, ""); if (pledge(argc == 1 ? "stdio" : "stdio rpath", NULL) == -1) err(1, "pledge"); if ((comparetime = getcomparetime()) == (time_t)-1) errx(1, "getcomparetime"); if (argc == 1) { printfeed(stdin, ""); checkfileerror(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, "", 'w'); fclose(fp); } } checkfileerror(stdout, "", 'w'); return 0; } sfeed-2.2/sfeed_twtxt.1000066400000000000000000000014651476411343700151050ustar00rootroot00000000000000.Dd July 31, 2021 .Dt SFEED_TWTXT 1 .Os .Sh NAME .Nm sfeed_twtxt .Nd format feed data to a twtxt 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 a twtxt feed. 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_twtxt .Ed .Sh SEE ALSO .Xr sfeed 1 , .Xr sfeed_plain 1 , .Xr sfeed 5 .Sh AUTHORS .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org sfeed-2.2/sfeed_twtxt.c000066400000000000000000000030411476411343700151570ustar00rootroot00000000000000#include #include #include #include "util.h" static char *line; static size_t linesize; static void printfeed(FILE *fp, const char *feedname) { char *fields[FieldLast]; struct tm parsedtm, *tm; time_t parsedtime; ssize_t linelen; while ((linelen = getline(&line, &linesize, fp)) > 0 && !ferror(stdout)) { if (line[linelen - 1] == '\n') line[--linelen] = '\0'; parseline(line, fields); parsedtime = 0; if (!strtotime(fields[FieldUnixTimestamp], &parsedtime) && (tm = gmtime_r(&parsedtime, &parsedtm))) { fprintf(stdout, "%04d-%02d-%02dT%02d:%02d:%02dZ\t", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); } else { fputs("\t", stdout); } if (feedname[0]) printf("[%s] ", feedname); fputs(fields[FieldTitle], stdout); if (fields[FieldLink][0]) { fputs(": ", stdout); fputs(fields[FieldLink], stdout); } putchar('\n'); } } int main(int argc, char *argv[]) { FILE *fp; char *name; int i; if (pledge(argc == 1 ? "stdio" : "stdio rpath", NULL) == -1) err(1, "pledge"); if (argc == 1) { printfeed(stdin, ""); checkfileerror(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, "", 'w'); fclose(fp); } } checkfileerror(stdout, "", 'w'); return 0; } sfeed-2.2/sfeed_update000077500000000000000000000151121476411343700150330ustar00rootroot00000000000000#!/bin/sh # update feeds, merge with old feeds. # NOTE: assumes "sfeed_*" executables are in $PATH. # defaults sfeedpath="$HOME/.sfeed/feeds" # used for processing feeds concurrently: wait until ${maxjobs} amount of # feeds are finished at a time. maxjobs=16 # load config (evaluate shellscript). # loadconfig(configfile) loadconfig() { # allow to specify config via argv[1]. if [ "$1" != "" ]; then # get absolute path of config file required for including. config="$1" configpath=$(readlink -f "${config}" 2>/dev/null) else # default config location. config="$HOME/.sfeed/sfeedrc" configpath="${config}" fi # config is loaded here to be able to override $sfeedpath or functions. if [ -r "${configpath}" ] && [ -f "${configpath}" ]; then . "${configpath}" else 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. # fetch(name, url, feedfile) fetch() { # fail on redirects, hide User-Agent, timeout is 15 seconds. curl -L --max-redirs 0 -H "User-Agent:" -f -s -m 15 \ "$2" 2>/dev/null } # convert encoding from one encoding to another. # convertencoding(name, from, to) convertencoding() { if [ "$2" != "" ] && [ "$3" != "" ] && [ "$2" != "$3" ]; then iconv -cs -f "$2" -t "$3" 2> /dev/null else # else no convert, just output. cat fi } # parse and convert input, by default XML to the sfeed(5) TSV format. # parse(name, feedurl, basesiteurl) parse() { sfeed "$3" } # filter fields. # filter(name, url) filter() { cat } # merge raw files: unique sort by id, title, link. # merge(name, oldfile, newfile) merge() { sort -t ' ' -u -k6,6 -k2,2 -k3,3 "$2" "$3" 2>/dev/null } # order by timestamp (descending). # order(name, url) order() { sort -t ' ' -k1rn,1 2>/dev/null } # internal handler to fetch and process a feed. # _feed(name, feedurl, [basesiteurl], [encoding]) _feed() { name="$1" feedurl="$2" basesiteurl="$3" encoding="$4" filename="$(printf '%s' "${name}" | tr '/' '_')" sfeedfile="${sfeedpath}/${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_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_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_error "${name}" "FAIL (PARSE)" return 1 fi rm -f "${tmpfeedfile}.utf8" 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 0 fi if ! merge "${name}" "${sfeedfile}" "${tmpfeedfile}.filter" > "${tmpfeedfile}.merge"; then log_error "${name}" "FAIL (MERGE)" return 1 fi rm -f "${tmpfeedfile}.filter" 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_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() { # 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 children >:D kill -TERM -$$ } # feeds() feeds() { 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 } # runfeeds() runfeeds() { # 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" } # main(args...) main() { # signal number received for parent. signo=0 # SIGINT: signal to interrupt parent. trap -- "sighandler 2" "INT" # SIGTERM: signal to terminate parent. trap -- "sighandler 15" "TERM" # load config file. loadconfig "$1" # fetch feeds and store in temporary directory. sfeedtmpdir="$(mktemp -d "${TMPDIR:-/tmp}/sfeed_XXXXXX")" || die mkdir -p "${sfeedtmpdir}/feeds" touch "${sfeedtmpdir}/ok" || die # make sure path exists. mkdir -p "${sfeedpath}" # run and process the feeds. runfeeds 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 ] && die $((signo+128)) die ${statuscode} } # process a single feed. # parameters are: config, tmpdir, name, feedurl, basesiteurl, encoding if [ "${SFEED_UPDATE_CHILD}" = "1" ]; then [ "$1" = "" ] && exit 0 # must have an argument set # IFS is "\037" printf '%s\n' "$1" | \ while IFS="" 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 "$@" sfeed-2.2/sfeed_update.1000066400000000000000000000035161476411343700151740ustar00rootroot00000000000000.Dd October 27, 2024 .Dt SFEED_UPDATE 1 .Os .Sh NAME .Nm sfeed_update .Nd update feeds and merge with old feeds .Sh SYNOPSIS .Nm .Op Ar sfeedrc .Sh DESCRIPTION .Nm writes TAB-separated feed files and merges new items with the items in any existing files. The items are stored as one file per feed in the directory .Pa $HOME/.sfeed/feeds by default. The directory can be changed in the .Xr sfeedrc 5 file. .Sh OPTIONS .Bl -tag -width Ds .It Ar sfeedrc Config file. The default is .Pa $HOME/.sfeed/sfeedrc . .El .Sh FILES READ .Bl -tag -width Ds .It Ar sfeedrc This file is evaluated as a shellscript in .Nm . See also the .Xr sfeedrc 5 man page for a detailed description of the format and an example file. .El .Sh FILES WRITTEN .Bl -tag -width Ds .It Pa feedname TAB-separated .Xr sfeed 5 format containing all items per feed. The .Nm script merges new items with this file. .El .Sh ENVIRONMENT VARIABLES .Bl -tag -width Ds .It SFEED_UPDATE_INCLUDE When set to "1" .Nm 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 -offset 4n [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 -offset 4n # Update feeds sfeed_update "configfile" # Format to a plain-text list sfeed_plain ~/.sfeed/feeds/* > ~/.sfeed/feeds.txt # Format to HTML sfeed_html ~/.sfeed/feeds/* > ~/.sfeed/feeds.html # Format to HTML with frames mkdir -p somedir && cd somedir && sfeed_frames ~/.sfeed/feeds/* .Ed .Sh SEE ALSO .Xr sfeed 1 , .Xr sfeed_frames 1 , .Xr sfeed_html 1 , .Xr sfeed_plain 1 , .Xr sh 1 , .Xr sfeed 5 , .Xr sfeedrc 5 .Sh AUTHORS .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org sfeed-2.2/sfeed_web.1000066400000000000000000000022011476411343700144550ustar00rootroot00000000000000.Dd July 27, 2021 .Dt SFEED_WEB 1 .Os .Sh NAME .Nm sfeed_web .Nd finds URLs to feeds from a HTML webpage .Sh SYNOPSIS .Nm .Op Ar baseurl .Sh DESCRIPTION .Nm reads the HTML data of the webpage from stdin and writes the found URLs to stdout. .Pp Such a link reference in HTML code looks like: .Bd -literal -offset 4n .Ed .Sh OPTIONS .Bl -tag -width 8n .It Ar baseurl Optional base URL to use for found feed URLs that are relative. .El .Sh OUTPUT FORMAT urlcontent-type .Bl -tag -width Ds .It URL Found relative or absolute URL. .Pp For relative URLs if a tag is found it will be used, otherwise if the .Ar baseurl option is specified then that is used, if neither are set then the relative URL is printed. .It content-type Usually application/atom+xml or application/rss+xml. .El .Sh EXIT STATUS .Ex -std .Sh EXAMPLES Get URLs from a website: .Bd -literal -offset 4n curl -s -L 'https://codemadness.org/' | sfeed_web 'https://codemadness.org/' .Ed .Sh SEE ALSO .Xr sfeed_update 1 , .Xr sh 1 .Sh AUTHORS .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org sfeed-2.2/sfeed_web.c000066400000000000000000000060411476411343700145450ustar00rootroot00000000000000#include #include #include "util.h" #include "xml.h" /* string and size */ #define STRP(s) s,sizeof(s)-1 static XMLParser parser; static int isbasetag, islinktag, ishrefattr, istypeattr; static char linkhref[4096], linktype[256], basehref[4096]; static void printvalue(const char *s) { for (; *s; s++) if (!ISCNTRL((unsigned char)*s)) putchar(*s); } static void xmltagstart(XMLParser *p, const char *t, size_t tl) { isbasetag = islinktag = 0; if (!strcasecmp(t, "base")) { isbasetag = 1; } else if (!strcasecmp(t, "link")) { islinktag = 1; linkhref[0] = '\0'; linktype[0] = '\0'; } } static void xmltagstartparsed(XMLParser *p, const char *t, size_t tl, int isshort) { struct uri baseuri, linkuri, u; char buf[4096]; int r = -1; if (!islinktag) return; if (strncasecmp(linktype, STRP("application/atom")) && strncasecmp(linktype, STRP("application/xml")) && strncasecmp(linktype, STRP("application/rss"))) return; /* parse base URI each time: it can change. */ if (basehref[0] && uri_parse(linkhref, &linkuri) != -1 && !linkuri.proto[0] && uri_parse(basehref, &baseuri) != -1 && uri_makeabs(&u, &linkuri, &baseuri) != -1 && u.proto[0]) r = uri_format(buf, sizeof(buf), &u); if (r >= 0 && (size_t)r < sizeof(buf)) printvalue(buf); else printvalue(linkhref); putchar('\t'); printvalue(linktype); putchar('\n'); } static void xmlattrstart(XMLParser *p, const char *t, size_t tl, const char *a, size_t al) { ishrefattr = istypeattr = 0; if (!isbasetag && !islinktag) return; if (!strcasecmp(a, "href")) { ishrefattr = 1; if (isbasetag) basehref[0] = '\0'; else if (islinktag) linkhref[0] = '\0'; } else if (!strcasecmp(a, "type") && islinktag) { istypeattr = 1; linktype[0] = '\0'; } } static void xmlattr(XMLParser *p, const char *t, size_t tl, const char *n, size_t nl, const char *v, size_t vl) { if (isbasetag && ishrefattr) { strlcat(basehref, v, sizeof(basehref)); } else if (islinktag) { if (ishrefattr) strlcat(linkhref, v, sizeof(linkhref)); else if (istypeattr) strlcat(linktype, v, sizeof(linktype)); } } 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[8]; int len; if (!ishrefattr && !istypeattr) return; /* try to translate entity, else just pass as data to * xmlattr handler. */ if ((len = xml_entitytostr(v, buf, sizeof(buf))) > 0) xmlattr(p, t, tl, a, al, buf, (size_t)len); else xmlattr(p, t, tl, a, al, v, vl); } int main(int argc, char *argv[]) { if (pledge("stdio", NULL) == -1) err(1, "pledge"); if (argc > 1) strlcpy(basehref, argv[1], sizeof(basehref)); parser.xmlattr = xmlattr; parser.xmlattrentity = xmlattrentity; parser.xmlattrstart = xmlattrstart; parser.xmltagstart = xmltagstart; parser.xmltagstartparsed = xmltagstartparsed; /* NOTE: GETNEXT is defined in xml.h for inline optimization */ xml_parse(&parser); checkfileerror(stdin, "", 'r'); checkfileerror(stdout, "", 'w'); return 0; } sfeed-2.2/sfeed_xmlenc.1000066400000000000000000000011121476411343700151660ustar00rootroot00000000000000.Dd July 25, 2021 .Dt SFEED_XMLENC 1 .Os .Sh NAME .Nm sfeed_xmlenc .Nd get text-encoding from XML .Sh SYNOPSIS .Nm .Sh DESCRIPTION .Nm reads XML data from stdin and writes the found text-encoding to stdout. It converts characters in the encoding-name to lowercase and strips characters which are not allowed in an encoding-name. .Sh EXIT STATUS .Ex -std .Sh EXAMPLES Get text-encoding from an Atom feed: .Bd -literal curl -s -L 'https://codemadness.org/atom.xml' | sfeed_xmlenc .Ed .Sh SEE ALSO .Xr sfeed_update 1 , .Xr sh 1 .Sh AUTHORS .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org sfeed-2.2/sfeed_xmlenc.c000066400000000000000000000024671476411343700152660ustar00rootroot00000000000000#include #include #include #include "util.h" #include "xml.h" static XMLParser parser; static int tags; static void xmltagstart(XMLParser *p, const char *t, size_t tl) { /* optimization: try to find a processing instruction only at the * start of the data at the first few starting tags. */ if (tags++ > 3) exit(0); } static void xmlattr(XMLParser *p, const char *t, size_t tl, const char *n, size_t nl, const char *v, size_t vl) { if (strcasecmp(t, "?xml") || strcasecmp(n, "encoding")) return; for (; *v; v++) { if (ISALPHA((unsigned char)*v) || ISDIGIT((unsigned char)*v) || *v == '.' || *v == ':' || *v == '-' || *v == '_') putchar(TOLOWER((unsigned char)*v)); } } static void xmlattrend(XMLParser *p, const char *t, size_t tl, const char *n, size_t nl) { if (strcasecmp(t, "?xml") || strcasecmp(n, "encoding")) return; putchar('\n'); exit(0); } int main(void) { if (pledge("stdio", NULL) == -1) err(1, "pledge"); parser.xmlattr = xmlattr; parser.xmlattrentity = xmlattr; /* no entity conversion */ parser.xmlattrend = xmlattrend; parser.xmltagstart = xmltagstart; /* NOTE: GETNEXT is defined in xml.h for inline optimization */ xml_parse(&parser); checkfileerror(stdin, "", 'r'); checkfileerror(stdout, "", 'w'); return 0; } sfeed-2.2/sfeedrc.5000066400000000000000000000150171476411343700141620ustar00rootroot00000000000000.Dd February 9, 2025 .Dt SFEEDRC 5 .Os .Sh NAME .Nm sfeedrc .Nd sfeed_update(1) configuration file .Sh DESCRIPTION .Nm is the configuration file for .Xr sfeed_update 1 and is evaluated as a shellscript. .Sh VARIABLES .Bl -tag -width Ds .It Va sfeedpath can be set for the directory to store the TAB-separated feed files. The default is .Pa $HOME/.sfeed/feeds . .It Va maxjobs can be used to change the amount of concurrent .Fn feed jobs. The default is 16. .El .Sh FUNCTIONS .Bl -tag -width Ds .It Fn feeds This function is the required "main" entry-point function called from .Xr sfeed_update 1 . .It Fn feed "name" "feedurl" "basesiteurl" "encoding" Inside the .Fn feeds function feeds can be defined by calling the .Fn feed function. Its arguments are: .Bl -tag -width Ds .It Fa name 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. This is usually a HTTP or HTTPS URL. .It Op Fa basesiteurl Base URL of the feed links. This argument allows fixing relative item links. .Pp According to the RSS and Atom specification, feeds should always have absolute URLs, but this is not always the case in practice. .It Op Fa encoding Feeds are converted from this .Ar encoding to UTF-8. The .Ar encoding should be a usable character-set name for the .Xr iconv 1 tool. .El .El .Sh OVERRIDE FUNCTIONS Because .Xr sfeed_update 1 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 write the data to stdout. Its arguments are: .Bl -tag -width Ds .It Fa name Feed name. .It Fa url URL to fetch. .It Fa feedfile Used feedfile (useful for comparing modification times). .El .Pp By default the tool .Xr curl 1 is used. .It Fn convertencoding "name" "from" "to" 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. .It Fa from From text-encoding. .It Fa to To text-encoding. .El .Pp By default the tool .Xr iconv 1 is used. .It Fn parse "name" "feedurl" "basesiteurl" Read RSS/Atom XML data from stdin, convert and write it as .Xr sfeed 5 data to stdout. Its arguments are: .Bl -tag -width Ds .It Fa name Feed name. .It Fa feedurl URL of the feed. .It Fa basesiteurl Base URL of the feed links. This argument allows to fix relative item links. .El .It Fn filter "name" "url" Filter .Xr sfeed 5 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 .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. .It Fa oldfile Old file. .It Fa newfile New file. .El .It Fn order "name" "url" Sort .Xr sfeed 5 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 An example configuration file is included named sfeedrc.example and also shown below: .Bd -literal -offset 4n #sfeedpath="$HOME/.sfeed/feeds" # list of feeds to fetch: feeds() { # feed [basesiteurl] [encoding] 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" feed "reddit openbsd" "https://old.reddit.com/r/openbsd/.rss" feed "slashdot" "http://rss.slashdot.org/Slashdot/slashdot" "http://slashdot.org" feed "tweakers" "http://feeds.feedburner.com/tweakers/mixed" "http://tweakers.net" "iso-8859-1" # get youtube Atom feed: curl -s -L 'https://www.youtube.com/user/gocoding/videos' | sfeed_web | cut -f 1 feed "youtube golang" "https://www.youtube.com/feeds/videos.xml?channel_id=UCO3LEtymiLrgvpb59cNsb8A" feed "xkcd" "https://xkcd.com/atom.xml" "https://xkcd.com" } .Ed .Pp To change the default .Xr curl 1 options for fetching the data, the .Fn fetch function can be overridden and added at the top of the .Nm file, for example: .Bd -literal -offset 4n # fetch(name, url, feedfile) fetch() { # 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 .Pp Caching, incremental data updates and bandwidth saving .Pp For HTTP servers that support it some bandwidth saving can be done by changing some of the default curl options. These options can come at a cost of some privacy, because it exposes additional metadata from the previous request. .Pp .Bl -bullet -compact .It The curl ETag options (--etag-save and --etag-compare) can be used to store and send the previous ETag header value. curl version 7.73+ is recommended for it to work properly. .It The curl -z option can be used to send the modification date of a local file as a HTTP If-Modified-Since request header. The server can then respond if the data is modified or not or respond with only the incremental data. .It The curl --compressed option can be used to indicate the client supports decompression. Because RSS/Atom feeds are textual XML data this generally compresses very well. .It The example below also sets the User-Agent to sfeed, because some CDNs block HTTP clients based on the User-Agent request header. .El .Pp Example: .Bd -literal -offset 4n mkdir -p "$HOME/.sfeed/etags" "$HOME/.sfeed/lastmod" # fetch(name, url, feedfile) fetch() { basename="$(basename "$3")" etag="$HOME/.sfeed/etags/${basename}" lastmod="$HOME/.sfeed/lastmod/${basename}" output="${sfeedtmpdir}/feeds/${filename}.xml" curl \e -f -s -m 15 \e -L --max-redirs 0 \e -H "User-Agent: sfeed" \e --compressed \e --etag-save "${etag}" --etag-compare "${etag}" \e -R -o "${output}" \e -z "${lastmod}" \e "$2" 2>/dev/null || return 1 # succesful, but no file written: assume it is OK and Not Modified. [ -e "${output}" ] || return 0 # use server timestamp from curl -R to set Last-Modified. touch -r "${output}" "${lastmod}" 2>/dev/null cat "${output}" 2>/dev/null # use write output status, other errors are ignored here. fetchstatus="$?" rm -f "${output}" 2>/dev/null return "${fetchstatus}" } .Ed .Pp The README file has more examples. .Sh SEE ALSO .Xr curl 1 , .Xr iconv 1 , .Xr sfeed_update 1 , .Xr sh 1 .Sh AUTHORS .An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org sfeed-2.2/sfeedrc.example000066400000000000000000000017611476411343700154520ustar00rootroot00000000000000# 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 [basesiteurl] [encoding] 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" feed "reddit openbsd" "https://old.reddit.com/r/openbsd/.rss" feed "slashdot" "http://rss.slashdot.org/Slashdot/slashdot" "http://slashdot.org" feed "tweakers" "http://feeds.feedburner.com/tweakers/mixed" "http://tweakers.net" "iso-8859-1" # get youtube Atom feed: curl -s -L 'https://www.youtube.com/user/gocoding/videos' | sfeed_web | cut -f 1 feed "youtube golang" "https://www.youtube.com/feeds/videos.xml?channel_id=UCO3LEtymiLrgvpb59cNsb8A" feed "xkcd" "https://xkcd.com/atom.xml" "https://xkcd.com" } sfeed-2.2/strlcat.c000066400000000000000000000032731476411343700143020ustar00rootroot00000000000000/* $OpenBSD: strlcat.c,v 1.15 2015/03/02 21:41:08 millert Exp $ */ /* * Copyright (c) 1998, 2015 Todd C. Miller * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include /* * Appends src to string dst of size dsize (unlike strncat, dsize is the * full size of dst, not space left). At most dsize-1 characters * will be copied. Always NUL terminates (unless dsize <= strlen(dst)). * Returns strlen(src) + MIN(dsize, strlen(initial dst)). * If retval >= dsize, truncation occurred. */ size_t strlcat(char *dst, const char *src, size_t dsize) { const char *odst = dst; const char *osrc = src; size_t n = dsize; size_t dlen; /* Find the end of dst and adjust bytes left but don't go past end. */ while (n-- != 0 && *dst != '\0') dst++; dlen = dst - odst; n = dsize - dlen; if (n-- == 0) return(dlen + strlen(src)); while (*src != '\0') { if (n != 0) { *dst++ = *src; n--; } src++; } *dst = '\0'; return(dlen + (src - osrc)); /* count does not include NUL */ } sfeed-2.2/strlcpy.c000066400000000000000000000030501476411343700143170ustar00rootroot00000000000000/* $OpenBSD: strlcpy.c,v 1.12 2015/01/15 03:54:12 millert Exp $ */ /* * Copyright (c) 1998, 2015 Todd C. Miller * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include /* * Copy string src to buffer dst of size dsize. At most dsize-1 * chars will be copied. Always NUL terminates (unless dsize == 0). * Returns strlen(src); if retval >= dsize, truncation occurred. */ size_t strlcpy(char *dst, const char *src, size_t dsize) { const char *osrc = src; size_t nleft = dsize; /* Copy as many bytes as will fit. */ if (nleft != 0) { while (--nleft != 0) { if ((*dst++ = *src++) == '\0') break; } } /* Not enough room in dst, add NUL and traverse rest of src. */ if (nleft == 0) { if (dsize != 0) *dst = '\0'; /* NUL-terminate dst */ while (*src++) ; } return(src - osrc - 1); /* count does not include NUL */ } sfeed-2.2/style.css000066400000000000000000000016451476411343700143350ustar00rootroot00000000000000body { background-color: #fff; color: #333; font-family: monospace; font-size: 9pt; margin: 0; overflow: hidden; padding: 0; } h2 { font-size: 14pt; margin: 0.5em 0; } #sidebar ul, #sidebar ul li { list-style: none; margin: 0; padding: 0; } #sidebar { background-color: inherit; } #sidebar a { padding: 5px 3px 5px 10px; } #sidebar a { display: block; } #sidebar a, #items h2 a { color: inherit; } div#items { padding: 0 15px; } body.noframe div#sidebar { height: 100%; left: 0; overflow: auto; position: fixed; top: 0; width: 250px; z-index: 999; } body.noframe div#items { height: 100%; left: 250px; overflow: auto; position: absolute; right: 0; top: 0; } body.noframe div#items.nosidebar { left: 0px; } body.frame { overflow: auto; } body.frame #sidebar br { display: none; } @media (prefers-color-scheme: dark) { body { background-color: #000; color: #bdbdbd; } a { color: #56c8ff; } } sfeed-2.2/themes/000077500000000000000000000000001476411343700137425ustar00rootroot00000000000000sfeed-2.2/themes/mono.h000066400000000000000000000014451476411343700150670ustar00rootroot00000000000000/* default mono theme */ #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() #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() sfeed-2.2/themes/mono_highlight.h000066400000000000000000000017701476411343700171170ustar00rootroot00000000000000/* 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() #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() #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() sfeed-2.2/themes/newsboat.h000066400000000000000000000015571476411343700157450ustar00rootroot00000000000000/* newsboat-like (blue, yellow) */ #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 */ #define THEME_SCROLLBAR_NORMAL() do { ttywrite("\x1b[34m"); } while(0) #define THEME_SCROLLBAR_TICK_FOCUS() do { ttywrite("\x1b[44m"); } while(0) /* blue bg */ #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() #define THEME_INPUT_NORMAL() sfeed-2.2/themes/templeos.h000066400000000000000000000037151476411343700157510ustar00rootroot00000000000000/* TempleOS-like (for fun and god) */ /* set true-color foreground / background, Terry would've preferred ANSI */ #define SETFGCOLOR(r,g,b) ttywritef("\x1b[38;2;%d;%d;%dm", r, g, b) #define SETBGCOLOR(r,g,b) ttywritef("\x1b[48;2;%d;%d;%dm", r, g, b) #define THEME_ITEM_NORMAL() do { SETFGCOLOR(0x00, 0x00, 0xaa); SETBGCOLOR(0xff, 0xff, 0xff); } while(0) #define THEME_ITEM_FOCUS() do { SETFGCOLOR(0x00, 0x00, 0xaa); SETBGCOLOR(0xff, 0xff, 0xff); } while(0) #define THEME_ITEM_BOLD() do { attrmode(ATTR_BOLD_ON); SETFGCOLOR(0xaa, 0x00, 0x00); SETBGCOLOR(0xff, 0xff, 0xff); } while(0) #define THEME_ITEM_SELECTED() do { if (p->focused) attrmode(ATTR_REVERSE_ON); } while(0) #define THEME_SCROLLBAR_FOCUS() do { SETFGCOLOR(0x00, 0x00, 0xaa); SETBGCOLOR(0xff, 0xff, 0xff); } while(0) #define THEME_SCROLLBAR_NORMAL() do { SETFGCOLOR(0x00, 0x00, 0xaa); SETBGCOLOR(0xff, 0xff, 0xff); } while(0) #define THEME_SCROLLBAR_TICK_FOCUS() do { SETBGCOLOR(0x00, 0x00, 0xaa); SETFGCOLOR(0xff, 0xff, 0xff); } while(0) #define THEME_SCROLLBAR_TICK_NORMAL() do { SETBGCOLOR(0x00, 0x00, 0xaa); SETFGCOLOR(0xff, 0xff, 0xff); } while(0) #define THEME_LINEBAR() do { SETFGCOLOR(0x00, 0x00, 0xaa); SETBGCOLOR(0xff, 0xff, 0xff); } while(0) #define THEME_STATUSBAR() do { ttywrite("\x1b[6m"); SETBGCOLOR(0x00, 0x00, 0xaa); SETFGCOLOR(0xff, 0xff, 0xff); } while(0) /* blink statusbar */ #define THEME_INPUT_LABEL() do { SETFGCOLOR(0x00, 0x00, 0xaa); SETBGCOLOR(0xff, 0xff, 0xff); } while(0) #define THEME_INPUT_NORMAL() do { SETFGCOLOR(0x00, 0x00, 0xaa); SETBGCOLOR(0xff, 0xff, 0xff); } while(0) #undef SCROLLBAR_SYMBOL_BAR #define SCROLLBAR_SYMBOL_BAR "\xe2\x95\x91" /* symbol: "double vertical" */ #undef LINEBAR_SYMBOL_BAR #define LINEBAR_SYMBOL_BAR "\xe2\x95\x90" /* symbol: "double horizontal" */ #undef LINEBAR_SYMBOL_RIGHT #define LINEBAR_SYMBOL_RIGHT "\xe2\x95\xa3" /* symbol: "double vertical and left" */ sfeed-2.2/util.c000066400000000000000000000221251476411343700136000ustar00rootroot00000000000000#include #include #include #include #include #include #include "util.h" /* print to stderr, print error message of errno and exit(). * Unlike BSD err() it does not prefix __progname */ __dead void err(int exitstatus, const char *fmt, ...) { va_list ap; int saved_errno; saved_errno = errno; if (fmt) { va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fputs(": ", stderr); } fprintf(stderr, "%s\n", strerror(saved_errno)); exit(exitstatus); } /* print to stderr and exit(). * Unlike BSD errx() it does not prefix __progname */ __dead void errx(int exitstatus, const char *fmt, ...) { va_list ap; if (fmt) { va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); } fputs("\n", stderr); 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) { size_t i; if (!n[0]) return (char *)h; for (; *h; ++h) { for (i = 0; n[i] && TOLOWER((unsigned char)n[i]) == TOLOWER((unsigned char)h[i]); ++i) ; if (n[i] == '\0') return (char *)h; } return NULL; } /* Check if string has a non-empty scheme / protocol part. */ int uri_hasscheme(const char *s) { const char *p = s; 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 */ return (*p == ':' && p != s); } /* Parse URI string `s` into an uri structure `u`. * Returns 0 on success or -1 on failure */ int uri_parse(const char *s, struct uri *u) { const char *p = s; char *endptr; size_t i; long l; u->proto[0] = u->userinfo[0] = u->host[0] = u->port[0] = '\0'; u->path[0] = u->query[0] = u->fragment[0] = '\0'; /* protocol-relative */ if (*p == '/' && *(p + 1) == '/') { p += 2; /* skip "//" */ goto parseauth; } /* scheme / protocol part */ 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 */ if (*p == ':' && p != s) { if (*(p + 1) == '/' && *(p + 2) == '/') p += 3; /* skip "://" */ else p++; /* skip ":" */ if ((size_t)(p - s) >= sizeof(u->proto)) return -1; /* protocol too long */ memcpy(u->proto, s, p - s); u->proto[p - s] = '\0'; if (*(p - 1) != '/') goto parsepath; } else { p = s; /* no scheme format, reset to start */ goto parsepath; } parseauth: /* userinfo (username:password) */ i = strcspn(p, "@/?#"); if (p[i] == '@') { if (i >= sizeof(u->userinfo)) return -1; /* userinfo too long */ memcpy(u->userinfo, p, i); u->userinfo[i] = '\0'; p += i + 1; } /* IPv6 address */ if (*p == '[') { /* bracket not found, host too short or too long */ i = strcspn(p, "]"); if (p[i] != ']' || i < 3) return -1; i++; /* including "]" */ } else { /* domain / host part, skip until port, path or end. */ i = strcspn(p, ":/?#"); } if (i >= sizeof(u->host)) return -1; /* host too long */ memcpy(u->host, p, i); u->host[i] = '\0'; p += i; /* port */ if (*p == ':') { p++; if ((i = strcspn(p, "/?#")) >= sizeof(u->port)) return -1; /* port too long */ memcpy(u->port, p, i); u->port[i] = '\0'; /* check for valid port: range 1 - 65535, may be empty */ errno = 0; l = strtol(u->port, &endptr, 10); if (i && (errno || *endptr || l <= 0 || l > 65535)) return -1; p += i; } parsepath: /* path */ if ((i = strcspn(p, "?#")) >= sizeof(u->path)) return -1; /* path too long */ memcpy(u->path, p, i); u->path[i] = '\0'; p += i; /* query */ if (*p == '?') { p++; if ((i = strcspn(p, "#")) >= sizeof(u->query)) return -1; /* query too long */ memcpy(u->query, p, i); u->query[i] = '\0'; p += i; } /* fragment */ if (*p == '#') { p++; if ((i = strlen(p)) >= sizeof(u->fragment)) return -1; /* fragment too long */ memcpy(u->fragment, p, i); u->fragment[i] = '\0'; } return 0; } /* Transform and try to make the URI `u` absolute using base URI `b` into `a`. * Follows some of the logic from "RFC 3986 - 5.2.2. Transform References". * Returns 0 on success, -1 on error or truncation. */ int uri_makeabs(struct uri *a, struct uri *u, struct uri *b) { char *p; int c; strlcpy(a->fragment, u->fragment, sizeof(a->fragment)); if (u->proto[0] || u->host[0]) { strlcpy(a->proto, u->proto[0] ? u->proto : b->proto, sizeof(a->proto)); strlcpy(a->host, u->host, sizeof(a->host)); strlcpy(a->userinfo, u->userinfo, sizeof(a->userinfo)); strlcpy(a->host, u->host, sizeof(a->host)); strlcpy(a->port, u->port, sizeof(a->port)); strlcpy(a->path, u->path, sizeof(a->path)); strlcpy(a->query, u->query, sizeof(a->query)); return 0; } strlcpy(a->proto, b->proto, sizeof(a->proto)); strlcpy(a->host, b->host, sizeof(a->host)); strlcpy(a->userinfo, b->userinfo, sizeof(a->userinfo)); strlcpy(a->host, b->host, sizeof(a->host)); strlcpy(a->port, b->port, sizeof(a->port)); if (!u->path[0]) { strlcpy(a->path, b->path, sizeof(a->path)); } else if (u->path[0] == '/') { strlcpy(a->path, u->path, sizeof(a->path)); } else { a->path[0] = (b->host[0] && b->path[0] != '/') ? '/' : '\0'; a->path[1] = '\0'; if ((p = strrchr(b->path, '/'))) { c = *(++p); *p = '\0'; /* temporary NUL-terminate */ if (strlcat(a->path, b->path, sizeof(a->path)) >= sizeof(a->path)) return -1; *p = c; /* restore */ } if (strlcat(a->path, u->path, sizeof(a->path)) >= sizeof(a->path)) return -1; } if (u->path[0] || u->query[0]) strlcpy(a->query, u->query, sizeof(a->query)); else strlcpy(a->query, b->query, sizeof(a->query)); return 0; } int uri_format(char *buf, size_t bufsiz, struct uri *u) { return snprintf(buf, bufsiz, "%s%s%s%s%s%s%s%s%s%s%s%s", u->proto, u->userinfo[0] ? u->userinfo : "", u->userinfo[0] ? "@" : "", u->host, u->port[0] ? ":" : "", u->port, u->host[0] && u->path[0] && u->path[0] != '/' ? "/" : "", u->path, u->query[0] ? "?" : "", u->query, u->fragment[0] ? "#" : "", u->fragment); } /* Splits fields in the line buffer by replacing TAB separators with NUL ('\0') * terminators and assign these fields as pointers. If there are less fields * than expected then the field is an empty string constant. */ void parseline(char *line, char *fields[FieldLast]) { char *prev, *s; size_t i; for (prev = line, i = 0; (s = strchr(prev, '\t')) && i < FieldLast - 1; i++) { *s = '\0'; fields[i] = prev; prev = s + 1; } fields[i++] = prev; /* make non-parsed fields empty. */ for (; i < FieldLast; i++) fields[i] = ""; } /* Parse time to time_t, assumes time_t is signed, ignores fractions. */ int strtotime(const char *s, time_t *t) { long long l; char *e; errno = 0; l = strtoll(s, &e, 10); if (errno || *s == '\0' || *e) return -1; /* NOTE: the type long long supports the 64-bit range. If time_t is * 64-bit it is "2038-ready", otherwise it is truncated/wrapped. */ if (t) *t = (time_t)l; 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) { for (; *s; ++s) { switch (*s) { case '<': fputs("<", fp); break; case '>': fputs(">", fp); break; case '\'': fputs("'", fp); break; case '&': fputs("&", fp); break; case '"': fputs(""", fp); break; default: putc(*s, fp); } } } /* print `len` columns of characters. If string is shorter pad the rest with * characters `pad`. */ void printutf8pad(FILE *fp, const char *s, size_t len, int pad) { wchar_t wc; size_t col = 0, i, slen; int inc, rl, w; if (!len) return; slen = strlen(s); for (i = 0; i < slen; i += inc) { inc = 1; /* next byte */ if ((unsigned char)s[i] < 32) { continue; /* skip control characters */ } else if ((unsigned char)s[i] >= 127) { rl = mbtowc(&wc, s + i, slen - i < 4 ? slen - i : 4); inc = rl; if (rl < 0) { mbtowc(NULL, NULL, 0); /* reset state */ inc = 1; /* invalid, seek next byte */ w = 1; /* replacement char is one width */ } else if ((w = wcwidth(wc)) == -1) { continue; } if (col + w > len || (col + w == len && s[i + inc])) { fputs(PAD_TRUNCATE_SYMBOL, fp); /* ellipsis */ col++; break; } else if (rl < 0) { fputs(UTF_INVALID_SYMBOL, fp); /* replacement */ col++; continue; } fwrite(&s[i], 1, rl, fp); col += w; } else { /* optimization: simple ASCII character */ if (col + 1 > len || (col + 1 == len && s[i + 1])) { fputs(PAD_TRUNCATE_SYMBOL, fp); /* ellipsis */ col++; break; } putc(s[i], fp); col++; } } for (; col < len; ++col) putc(pad, fp); } sfeed-2.2/util.h000066400000000000000000000044361476411343700136120ustar00rootroot00000000000000#include #include #ifdef __OpenBSD__ #include #else #define pledge(p1,p2) 0 #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 size_t strlcat(char *, const char *, size_t); #undef strlcpy size_t strlcpy(char *, const char *, size_t); #ifndef SFEED_DUMBTERM #define PAD_TRUNCATE_SYMBOL "\xe2\x80\xa6" /* symbol: "ellipsis" */ #define UTF_INVALID_SYMBOL "\xef\xbf\xbd" /* symbol: "replacement" */ #else #define PAD_TRUNCATE_SYMBOL "." #define UTF_INVALID_SYMBOL "?" #endif /* feed info */ struct feed { char *name; /* feed name */ unsigned long totalnew; /* amount of new items per feed */ unsigned long total; /* total items */ /* sfeed_curses */ char *path; /* path to feed or NULL for stdin */ FILE *fp; /* file pointer */ }; /* URI */ struct uri { char proto[48]; /* scheme including ":" or "://" */ char userinfo[256]; /* username [:password] */ char host[256]; char port[6]; /* numeric port */ char path[1024]; char query[1024]; char fragment[1024]; }; enum { FieldUnixTimestamp = 0, FieldTitle, FieldLink, FieldContent, FieldContentType, FieldId, FieldAuthor, FieldEnclosure, FieldCategory, FieldLast }; /* hint for compilers and static analyzers that a function does not return. * some compilers use: __attribute__((noreturn)), _Noreturn, noreturn */ #ifndef __dead #define __dead #endif __dead void err(int, const char *, ...); __dead void errx(int, const char *, ...); int uri_format(char *, size_t, struct uri *); 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 *); void xmlencode(const char *, FILE *); sfeed-2.2/xml.c000066400000000000000000000235521476411343700134300ustar00rootroot00000000000000#include #include #include #include #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) { size_t namelen = 0, valuelen; int c, endsep, endname = 0, valuestart = 0; while ((c = GETNEXT()) != EOF) { if (ISSPACE(c)) { if (namelen) endname = 1; continue; } else if (c == '?') ; /* ignore */ else if (c == '=') { x->name[namelen] = '\0'; valuestart = 1; endname = 1; } else if (namelen && ((endname && !valuestart && ISALPHA(c)) || (c == '>' || c == '/'))) { /* attribute without value */ x->name[namelen] = '\0'; if (x->xmlattrstart) x->xmlattrstart(x, x->tag, x->taglen, x->name, namelen); if (x->xmlattr) x->xmlattr(x, x->tag, x->taglen, x->name, namelen, "", 0); if (x->xmlattrend) x->xmlattrend(x, x->tag, x->taglen, x->name, namelen); endname = 0; x->name[0] = c; namelen = 1; } else if (namelen && valuestart) { /* attribute with value */ if (x->xmlattrstart) x->xmlattrstart(x, x->tag, x->taglen, x->name, namelen); valuelen = 0; if (c == '\'' || c == '"') { endsep = c; } else { endsep = ' '; /* ISSPACE() */ goto startvalue; } while ((c = GETNEXT()) != EOF) { startvalue: if (c == '&') { /* entities */ x->data[valuelen] = '\0'; /* call data function with data before entity if there is data */ if (valuelen && x->xmlattr) x->xmlattr(x, x->tag, x->taglen, x->name, namelen, x->data, valuelen); x->data[0] = c; valuelen = 1; while ((c = GETNEXT()) != EOF) { if (c == endsep || (endsep == ' ' && (c == '>' || ISSPACE(c)))) break; if (valuelen < sizeof(x->data) - 1) x->data[valuelen++] = c; else { /* entity too long for buffer, handle as normal data */ x->data[valuelen] = '\0'; if (x->xmlattr) x->xmlattr(x, x->tag, x->taglen, x->name, namelen, x->data, valuelen); x->data[0] = c; valuelen = 1; break; } if (c == ';') { x->data[valuelen] = '\0'; if (x->xmlattrentity) x->xmlattrentity(x, x->tag, x->taglen, x->name, namelen, x->data, valuelen); valuelen = 0; break; } } } else if (c != endsep && !(endsep == ' ' && (c == '>' || ISSPACE(c)))) { if (valuelen < sizeof(x->data) - 1) { x->data[valuelen++] = c; } else { x->data[valuelen] = '\0'; if (x->xmlattr) x->xmlattr(x, x->tag, x->taglen, x->name, namelen, x->data, valuelen); x->data[0] = c; valuelen = 1; } } 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); if (x->xmlattrend) x->xmlattrend(x, x->tag, x->taglen, x->name, namelen); break; } } namelen = endname = valuestart = 0; } else if (namelen < sizeof(x->name) - 1) { x->name[namelen++] = c; } if (c == '>') { break; } else if (c == '/') { x->isshorttag = 1; x->name[0] = '\0'; namelen = 0; } } } static void xml_parsecomment(XMLParser *x) { int c, i = 0; while ((c = GETNEXT()) != EOF) { if (c == '-') { if (++i > 2) i = 2; continue; } else if (c == '>' && i == 2) { return; } else if (i) { i = 0; } } } static void xml_parsecdata(XMLParser *x) { size_t datalen = 0, i = 0; int c; while ((c = GETNEXT()) != EOF) { if (c == ']' || c == '>') { if (x->xmlcdata && datalen) { x->data[datalen] = '\0'; x->xmlcdata(x, x->data, datalen); datalen = 0; } } if (c == ']') { if (++i > 2) { if (x->xmlcdata) for (; i > 2; i--) x->xmlcdata(x, "]", 1); i = 2; } continue; } else if (c == '>' && i == 2) { return; } else if (i) { if (x->xmlcdata) for (; i > 0; i--) x->xmlcdata(x, "]", 1); i = 0; } if (datalen < sizeof(x->data) - 1) { x->data[datalen++] = c; } else { x->data[datalen] = '\0'; if (x->xmlcdata) x->xmlcdata(x, x->data, datalen); x->data[0] = c; datalen = 1; } } } static int codepointtoutf8(long r, char *s) { if (r == 0) { return 0; /* NUL byte */ } else if (r <= 0x7F) { /* 1 byte: 0aaaaaaa */ s[0] = r; return 1; } else if (r <= 0x07FF) { /* 2 bytes: 00000aaa aabbbbbb */ s[0] = 0xC0 | ((r & 0x0007C0) >> 6); /* 110aaaaa */ s[1] = 0x80 | (r & 0x00003F); /* 10bbbbbb */ return 2; } else if (r <= 0xFFFF) { /* 3 bytes: aaaabbbb bbcccccc */ s[0] = 0xE0 | ((r & 0x00F000) >> 12); /* 1110aaaa */ s[1] = 0x80 | ((r & 0x000FC0) >> 6); /* 10bbbbbb */ s[2] = 0x80 | (r & 0x00003F); /* 10cccccc */ return 3; } else { /* 4 bytes: 000aaabb bbbbcccc ccdddddd */ s[0] = 0xF0 | ((r & 0x1C0000) >> 18); /* 11110aaa */ s[1] = 0x80 | ((r & 0x03F000) >> 12); /* 10bbbbbb */ s[2] = 0x80 | ((r & 0x000FC0) >> 6); /* 10cccccc */ s[3] = 0x80 | (r & 0x00003F); /* 10dddddd */ return 4; } } static int namedentitytostr(const char *e, char *buf, size_t bufsiz) { static const struct { const char *entity; int c; } entities[] = { { "amp;", '&' }, { "lt;", '<' }, { "gt;", '>' }, { "apos;", '\'' }, { "quot;", '"' }, }; size_t i; /* buffer is too small */ if (bufsiz < 2) return -1; for (i = 0; i < sizeof(entities) / sizeof(*entities); i++) { if (!strcmp(e, entities[i].entity)) { buf[0] = entities[i].c; buf[1] = '\0'; return 1; } } return -1; } static int numericentitytostr(const char *e, char *buf, size_t bufsiz) { long l; int len; char *end; /* buffer is too small */ if (bufsiz < 5) return -1; errno = 0; /* hex (16) or decimal (10) */ if (*e == 'x') l = strtol(++e, &end, 16); else l = strtol(e, &end, 10); /* invalid value or not a well-formed entity or invalid code point */ if (errno || e == end || *end != ';' || l < 0 || l > 0x10ffff || (l >= 0xd800 && l <= 0xdfff)) return -1; len = codepointtoutf8(l, buf); buf[len] = '\0'; return len; } /* convert named- or numeric entity string to buffer string * returns byte-length of string or -1 on failure. */ int xml_entitytostr(const char *e, char *buf, size_t bufsiz) { /* doesn't start with & */ if (e[0] != '&') return -1; /* numeric entity */ if (e[1] == '#') return numericentitytostr(e + 2, buf, bufsiz); else /* named entity */ return namedentitytostr(e + 1, buf, bufsiz); } void xml_parse(XMLParser *x) { size_t datalen, tagdatalen; int c, isend; while ((c = GETNEXT()) != EOF && c != '<') ; /* skip until < */ while (c != EOF) { if (c == '<') { /* parse tag */ if ((c = GETNEXT()) == EOF) return; 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) x->data[tagdatalen++] = c; if (c == '>') break; else if (c == '-' && tagdatalen == sizeof("--") - 1 && (x->data[0] == '-')) { xml_parsecomment(x); break; } else if (c == '[') { if (tagdatalen == sizeof("[CDATA[") - 1 && !strncmp(x->data, "[CDATA[", tagdatalen)) { xml_parsecdata(x); break; } } } } else { /* normal tag (open, short open, close), processing instruction. */ x->tag[0] = c; x->taglen = 1; x->isshorttag = isend = 0; /* treat processing instruction as short tag, don't strip "?" prefix. */ if (c == '?') { x->isshorttag = 1; } else if (c == '/') { if ((c = GETNEXT()) == EOF) return; x->tag[0] = c; isend = 1; } while ((c = GETNEXT()) != EOF) { if (c == '/') x->isshorttag = 1; /* short tag */ else if (c == '>' || ISSPACE(c)) { x->tag[x->taglen] = '\0'; if (isend) { /* end tag, starts with ' && c != EOF) /* skip until > */ c = GETNEXT(); if (x->xmltagend) x->xmltagend(x, x->tag, x->taglen, x->isshorttag); x->tag[0] = '\0'; x->taglen = 0; } else { /* start tag */ if (x->xmltagstart) x->xmltagstart(x, x->tag, x->taglen); if (ISSPACE(c)) xml_parseattrs(x); if (x->xmltagstartparsed) x->xmltagstartparsed(x, x->tag, x->taglen, x->isshorttag); } /* call tagend for short tag or processing instruction */ if (x->isshorttag) { if (x->xmltagend) x->xmltagend(x, x->tag, x->taglen, x->isshorttag); x->tag[0] = '\0'; x->taglen = 0; } break; } else if (x->taglen < sizeof(x->tag) - 1) x->tag[x->taglen++] = c; /* NOTE: tag name truncation */ } } } else { /* parse tag data */ datalen = 0; while ((c = GETNEXT()) != EOF) { if (c == '&') { if (datalen) { x->data[datalen] = '\0'; if (x->xmldata) x->xmldata(x, x->data, datalen); } x->data[0] = c; datalen = 1; while ((c = GETNEXT()) != EOF) { if (c == '<') break; if (datalen < sizeof(x->data) - 1) x->data[datalen++] = c; else { /* entity too long for buffer, handle as normal data */ x->data[datalen] = '\0'; if (x->xmldata) x->xmldata(x, x->data, datalen); x->data[0] = c; datalen = 1; break; } if (c == ';') { x->data[datalen] = '\0'; if (x->xmldataentity) x->xmldataentity(x, x->data, datalen); datalen = 0; break; } } } else if (c != '<') { if (datalen < sizeof(x->data) - 1) { x->data[datalen++] = c; } else { x->data[datalen] = '\0'; if (x->xmldata) x->xmldata(x, x->data, datalen); x->data[0] = c; datalen = 1; } } if (c == '<') { x->data[datalen] = '\0'; if (x->xmldata && datalen) x->xmldata(x, x->data, datalen); break; } } } } } sfeed-2.2/xml.h000066400000000000000000000026001476411343700134240ustar00rootroot00000000000000#ifndef XML_H #define XML_H #include typedef struct xmlparser { /* handlers */ void (*xmlattr)(struct xmlparser *, const char *, size_t, const char *, size_t, const char *, size_t); void (*xmlattrend)(struct xmlparser *, const char *, size_t, const char *, size_t); void (*xmlattrstart)(struct xmlparser *, const char *, size_t, const char *, size_t); void (*xmlattrentity)(struct xmlparser *, const char *, size_t, const char *, size_t, const char *, size_t); void (*xmlcdata)(struct xmlparser *, const char *, size_t); void (*xmldata)(struct xmlparser *, const char *, size_t); void (*xmldataentity)(struct xmlparser *, const char *, size_t); void (*xmltagend)(struct xmlparser *, const char *, size_t, int); void (*xmltagstart)(struct xmlparser *, const char *, size_t); void (*xmltagstartparsed)(struct xmlparser *, const char *, size_t, int); #ifndef GETNEXT /* GETNEXT overridden to reduce function call overhead and further * context optimizations. */ #define GETNEXT getchar_unlocked #endif /* current tag */ char tag[1024]; size_t taglen; /* current tag is a short tag ? */ int isshorttag; /* current attribute name */ char name[1024]; /* data buffer used for tag data, CDATA and attribute data */ char data[BUFSIZ]; } XMLParser; int xml_entitytostr(const char *, char *, size_t); void xml_parse(XMLParser *); #endif