sfeed_curses

[fork] sfeed (atom feed) reader
Log | Files | Refs | README | LICENSE

commit efc1a134e2ceb99e8b496e5d53f77b777700717e
parent 736bbf5fe08489e133a49e2b71ddbef845e0ed47
Author: Hiltjo Posthuma <hiltjo@codemadness.org>
Date:   Thu, 16 Jul 2020 22:26:46 +0200

add support for another option to mark items as read/unread

Diffstat:
MMakefile | 5+++--
MREADME | 11+++++++++++
Msfeed_curses.1 | 42+++++++++++++++++++++++++++++++++++++++++-
Msfeed_curses.c | 165++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Asfeed_markread | 28++++++++++++++++++++++++++++
Asfeed_markread.1 | 26++++++++++++++++++++++++++
6 files changed, 265 insertions(+), 12 deletions(-)

diff --git a/Makefile b/Makefile @@ -17,11 +17,12 @@ SFEED_CPPFLAGS = -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=700 -D_BSD_SOURCE #SFEED_LDFLAGS = ${LDFLAGS} -lncurses BIN = sfeed_curses -SCRIPTS = +SCRIPTS = sfeed_markread SRC = ${BIN:=.c} -MAN1 = ${BIN:=.1} +MAN1 = ${BIN:=.1}\ + ${SCRIPTS:=.1} DOC = \ LICENSE\ README\ diff --git a/README b/README @@ -28,6 +28,16 @@ Like the format programs included in sfeed you can run it like this: sfeed_curses < ~/.sfeed/feeds/xkcd +By default sfeed_curses marks the items of the last day as new/bold. To manage +read/unread items in a different way a plain-text file with a list of the read +urls can be used. To enable this behaviour the path to this file can be +specified by setting the environment variable $SFEED_URL_FILE to the url file: + + SFEED_URL_FILE=~/.sfeed/urls sfeed_curses ~/.sfeed/feeds/* + +There is an shellscript "sfeed_markread" to process the read and unread items. +See the man page for more detailed information. + Dependencies ------------ @@ -50,6 +60,7 @@ Run-time dependencies - A (POSIX) shell. - A terminal (emulator) supporting UTF-8 and more modern capabilities. - xclip for yanking the url or enclosure: can be changed easily. +- awk, used by the sfeed_markread script (optional). OS tested diff --git a/sfeed_curses.1 b/sfeed_curses.1 @@ -1,4 +1,4 @@ -.Dd July 15, 2020 +.Dd July 16, 2020 .Dt SFEED_CURSES 1 .Os .Sh NAME @@ -95,6 +95,26 @@ The used command to yank the url is "cut -f 3 | xclip -r". .It E Pipe the TAB-Separated Value for yanking the enclosure. The used command to yank the enclosure is "cut -f 8 | xclip -r". +.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 current loaded feed as read. +This will only work when +.Ev SFEED_URL_FILE +is set. +.It F +Mark all items of the current loaded feed as unread. +This will only work when +.Ev SFEED_URL_FILE +is set. .It q, EOF Quit .El @@ -129,6 +149,26 @@ By default this is "less". .It Ev SFEED_PLUMBER A program that received the url or enclosure url as a parameter. By default this is "xdg-open". +.It Ev SFEED_URL_FILE +If this variable is set this file will be used if an item is new/unread or +read, instead of checking the timestamp, which is the default. +The format is a plain-text file with a list of read urls, one url per line. +This url is matched on the link field as specified in +.Xr sfeed 5 . +.It Ev SFEED_MARK_READ +A program to mark items as read if +.Ev SFEED_URL_FILE +is also set, if unset the program used is "sfeed_markread read". +The marked items are piped to the program. +The program is expected to merge items in a safe/transactional manner. +The program should return the exit status 0 on succeed 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 program used is "sfeed_markread unread". +The marked items are piped to the program. +The program is expected to merge items in a safe/transactional manner. +The program should return the exit status 0 on succeed or non-zero on failure. .It Ev SFEED_FEED_PATH This variable is set by .Nm diff --git a/sfeed_curses.c b/sfeed_curses.c @@ -96,12 +96,13 @@ struct feed { }; struct item { + char *link; /* separate link field (always loaded) */ char *fields[FieldLast]; char *line; /* allocated split line */ time_t timestamp; int timeok; int isnew; - off_t offset; /* line offset in file */ + off_t offset; /* line offset in file for lazyload */ }; #undef err @@ -110,7 +111,10 @@ void err(int, const char *, ...); void alldirty(void); void cleanup(void); void draw(void); +int isurlnew(const char *); +void markread(struct pane *, off_t, off_t, int); void pane_draw(struct pane *); +void readurls(void); void sighandler(int); void updategeom(void); void updatesidebar(int); @@ -132,6 +136,8 @@ static struct feed *feeds; static struct feed *curfeed; static size_t nfeeds; /* amount of feeds */ static time_t comparetime; +static char *urlfile, **urls; +static size_t nurls; volatile sig_atomic_t sigstate = 0; @@ -986,6 +992,7 @@ linetoitem(char *line, struct item *item) item->line = line; parseline(line, fields); memcpy(item->fields, fields, sizeof(fields)); + item->link = estrdup(fields[FieldLink]); parsedtime = 0; if (!strtotime(fields[FieldUnixTimestamp], &parsedtime)) { @@ -1069,7 +1076,10 @@ updatenewitems(struct feed *f) for (i = 0; i < p->nrows; i++) { row = &(p->rows[i]); /* do not use pane_row_get */ item = (struct item *)row->data; - item->isnew = (item->timeok && item->timestamp >= comparetime); + if (urlfile) + item->isnew = isurlnew(item->link); + else + item->isnew = (item->timeok && item->timestamp >= comparetime); row->bold = item->isnew; f->totalnew += item->isnew; } @@ -1085,8 +1095,10 @@ feed_load(struct feed *f, FILE *fp) struct row *row; size_t i; - for (i = 0; i < nitems; i++) + for (i = 0; i < nitems; i++) { free(items[i].line); + free(items[i].link); + } free(items); items = NULL; nitems = 0; @@ -1129,9 +1141,13 @@ feed_count(struct feed *f, FILE *fp) line[--linelen] = '\0'; parseline(line, fields); - parsedtime = 0; - if (!strtotime(fields[FieldUnixTimestamp], &parsedtime)) - f->totalnew += (parsedtime >= comparetime); + if (urlfile) { + f->totalnew += isurlnew(fields[FieldLink]); + } else { + parsedtime = 0; + if (!strtotime(fields[FieldUnixTimestamp], &parsedtime)) + f->totalnew += (parsedtime >= comparetime); + } f->total++; } free(line); @@ -1216,6 +1232,7 @@ feeds_reloadall(void) off_t pos; pos = panes[PaneItems].pos; /* store numeric position */ + readurls(); feeds_load(feeds, nfeeds); /* restore numeric position */ pane_setpos(&panes[PaneItems], pos); @@ -1310,7 +1327,7 @@ draw(void) if (panes[PaneItems].nrows && (row = pane_row_get(&panes[PaneItems], panes[PaneItems].pos))) { item = (struct item *)row->data; - statusbar_update(&statusbar, item->fields[FieldLink]); + statusbar_update(&statusbar, item->link); } else { statusbar_update(&statusbar, ""); } @@ -1377,6 +1394,7 @@ mousereport(int button, int release, int x, int y) if (dblclick && !changedpane) { row = pane_row_get(&panes[PaneItems], pos); item = (struct item *)row->data; + markread(p, p->pos, p->pos, 1); plumb(plumber, item->fields[FieldLink]); } } @@ -1389,6 +1407,7 @@ mousereport(int button, int release, int x, int y) p = &panes[PaneItems]; row = pane_row_get(p, p->pos); item = (struct item *)row->data; + markread(p, p->pos, p->pos, 1); pipeitem(piper, item, 1); } break; @@ -1492,6 +1511,114 @@ item_row_format(struct pane *p, struct row *row) return text; } +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, wpid, status; + + if (!urlfile || !p->nrows) + return; + + if (isread) { + if (!(cmd = getenv("SFEED_MARK_READ"))) + cmd = "sfeed_markread read"; + } else { + if (!(cmd = getenv("SFEED_MARK_UNREAD"))) + cmd = "sfeed_markread unread"; + } + + switch ((pid = fork())) { + case -1: + err(1, "fork"); + case 0: + dup2(devnullfd, 1); + dup2(devnullfd, 2); + + errno = 0; + if (!(fp = popen(cmd, "w"))) + err(1, "popen"); + + for (i = from; i <= to; i++) { + row = &(p->rows[i]); + item = (struct item *)row->data; + if (item->isnew != isnew) { + fputs(item->link, fp); + fputc('\n', fp); + } + } + status = pclose(fp); + status = WIFEXITED(status) ? WEXITSTATUS(status) : 127; + _exit(status); + default: + while ((wpid = wait(&status)) >= 0 && wpid != pid) + ; + + /* fail: exit statuscode was non-zero */ + if (status) + break; + for (i = from; i <= to && i < p->nrows; i++) { + row = &(p->rows[i]); + item = (struct item *)row->data; + if (item->isnew != isnew) { + row->bold = item->isnew = isnew; + curfeed->totalnew += isnew ? 1 : -1; + } + } + updatesidebar(onlynew); + updategeom(); + updatetitle(); + } +} + +int +urlcmp(const void *v1, const void *v2) +{ + return strcmp(*((char **)v1), *((char **)v2)); +} + +void +readurls(void) +{ + FILE *fp; + char *line = NULL; + size_t linesiz = 0, cap = 0; + ssize_t n; + + while (nurls > 0) + free(urls[--nurls]); + free(urls); + urls = NULL; + nurls = 0; + + if (!urlfile || !(fp = fopen(urlfile, "rb"))) + return; + + while ((n = getline(&line, &linesiz, fp)) > 0) { + if (line[n - 1] == '\n') + line[--n] = '\0'; + if (nurls + 1 >= cap) { + cap = cap ? cap * 2 : 16; + urls = erealloc(urls, cap * sizeof(char *)); + } + urls[nurls++] = estrdup(line); + } + fclose(fp); + free(line); + + qsort(urls, nurls, sizeof(char *), urlcmp); +} + +int +isurlnew(const char *url) +{ + return bsearch(&url, urls, nurls, sizeof(char *), urlcmp) == NULL; +} + int main(int argc, char *argv[]) { @@ -1511,6 +1638,7 @@ main(int argc, char *argv[]) plumber = tmp; if ((tmp = getenv("SFEED_PIPER"))) piper = tmp; + urlfile = getenv("SFEED_URL_FILE"); panes[PaneFeeds].row_format = feed_row_format; panes[PaneFeeds].row_match = feed_row_match; @@ -1533,6 +1661,7 @@ main(int argc, char *argv[]) } nfeeds = argc - 1; } + readurls(); feeds_load(feeds, nfeeds); feeds_set(&feeds[0]); @@ -1746,7 +1875,8 @@ nextpage: p = &panes[PaneItems]; row = pane_row_get(p, p->pos); item = (struct item *)row->data; - plumb(plumber, item->fields[FieldLink]); + markread(p, p->pos, p->pos, 1); + plumb(plumber, item->link); } break; case 'c': /* items: pipe TSV line to program */ @@ -1761,10 +1891,27 @@ nextpage: switch (ch) { case 'y': pipeitem("cut -f 3 | xclip -r", item, 0); break; case 'E': pipeitem("cut -f 8 | xclip -r", item, 0); break; - default: pipeitem(piper, item, 1); break; + default: + markread(p, p->pos, p->pos, 1); + pipeitem(piper, item, 1); + break; } } 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[PaneItems].nrows) { + p = &panes[PaneItems]; + markread(p, p->pos, p->pos, ch == 'r'); + } + break; case 4: /* EOT */ case 'q': goto end; } diff --git a/sfeed_markread b/sfeed_markread @@ -0,0 +1,28 @@ +#!/bin/sh +# usage: $0 <read|unread> +# input is the read / unread url per line. + +if test -z "$SFEED_URL_FILE"; then + echo "\$SFEED_URL_FILE must be set" >&2 + exit 1 +fi + +case "$1" in +read) + cat >> "$SFEED_URL_FILE";; +unread) + tmp=$(mktemp) + trap "rm -f $tmp" EXIT + LC_CTYPE=C awk -F '\t' ' + { FILENR += (FNR == 1) } + FILENR == 1 { urls[$0] = 1 } + FILENR == 2 { if (urls[$0]) { changed = 1 } else { print $0 } } + END { exit(!changed) }' \ + "/dev/stdin" "$SFEED_URL_FILE" > "$tmp" && \ + mv "$tmp" "$SFEED_URL_FILE" + ;; +*) + echo "$0 <read|unread>" >&2 + exit 1 + ;; +esac diff --git a/sfeed_markread.1 b/sfeed_markread.1 @@ -0,0 +1,26 @@ +.Dd July 16, 2020 +.Dt SFEED_MARKREAD 1 +.Os +.Sh NAME +.Nm sfeed_markread +.Nd script to mark items as read/unread +.Sh SYNOPSIS +.Nm +.Ar read | Ar unread +.Sh DESCRIPTION +.Nm +expects to receive a plain-text list with one or more urls from stdin. +The received format from stdin is one url per line. +.Sh ENVIRONMENT VARIABLES +.Bl -tag -width Ds +.It Ev SFEED_URL_FILE +This variable must 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 SEE ALSO +.Xr awk 1 , +.Xr sfeed_curses 1 +.Sh AUTHORS +.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org