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:
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