commit 1ac999e4809fa8ba122720ae41a438ef4d7cef76
Author: Hiltjo Posthuma <hiltjo@codemadness.org>
Date: Wed, 24 Jun 2020 18:59:00 +0200
initial repo
Diffstat:
A | LICENSE | | | 15 | +++++++++++++++ |
A | Makefile | | | 75 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | README | | | 9 | +++++++++ |
A | content.example.sh | | | 50 | ++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | sfeed_curses.1 | | | 122 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | sfeed_curses.c | | | 1467 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
6 files changed, 1738 insertions(+), 0 deletions(-)
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) 2020 Hiltjo Posthuma <hiltjo@codemadness.org>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+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.
diff --git a/Makefile b/Makefile
@@ -0,0 +1,75 @@
+.POSIX:
+
+NAME = sfeed_curses
+VERSION = 0.1
+
+# paths
+PREFIX = /usr/local
+MANPREFIX = ${PREFIX}/man
+DOCPREFIX = ${PREFIX}/share/doc/${NAME}
+
+# use system flags.
+SFEED_CFLAGS = ${CFLAGS}
+SFEED_LDFLAGS = ${LDFLAGS} -lcurses
+SFEED_CPPFLAGS = -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=700 -D_BSD_SOURCE
+
+BIN = sfeed_curses
+
+SRC = ${BIN:=.c}
+
+MAN1 = ${BIN:=.1}
+DOC = \
+ LICENSE\
+ README\
+ content.example.sh
+
+all: ${BIN}
+
+${BIN}: ${@:=.o}
+
+OBJ = ${SRC:.c=.o}
+
+.o:
+ ${CC} ${SFEED_LDFLAGS} -o $@ $<
+
+.c.o:
+ ${CC} ${SFEED_CFLAGS} ${SFEED_CPPFLAGS} -o $@ -c $<
+
+dist:
+ rm -rf "${NAME}-${VERSION}"
+ mkdir -p "${NAME}-${VERSION}"
+ cp -f ${MAN1} ${DOC} ${SRC} Makefile \
+ "${NAME}-${VERSION}"
+ # make tarball
+ tar cf - "${NAME}-${VERSION}" | \
+ gzip -c > "${NAME}-${VERSION}.tar.gz"
+ rm -rf "${NAME}-${VERSION}"
+
+clean:
+ rm -f ${BIN} ${OBJ}
+
+install: all
+ # installing executable files and scripts.
+ mkdir -p "${DESTDIR}${PREFIX}/bin"
+ cp -f ${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 README\
+ "${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
+
+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}/README"
+ -rmdir "${DESTDIR}${DOCPREFIX}"
+ # removing manual pages.
+ for m in ${MAN1}; do rm -f "${DESTDIR}${MANPREFIX}/man1/$$m"; done
+
+.PHONY: all clean dist install uninstall
diff --git a/README b/README
@@ -0,0 +1,9 @@
+sfeed curses UI
+
+
+Dependencies
+------------
+
+* C compiler
+* curses
+* A terminal (emulator) supporting UTF-8 and more modern capabilities.
diff --git a/content.example.sh b/content.example.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+# RSS/Atom content viewer.
+#
+# Dependencies:
+# - awk, sh, etc.
+# - lynx or webdump or HTML converting to plain-text.
+
+# content()
+content() {
+ awk -F '\t' '
+function unescape(s) {
+ gsub("\\\\t", "\t", s);
+ gsub("\\\\n", "\n", s);
+ gsub("\\\\\\\\", "\\", s);
+ return s;
+}
+{
+ print unescape($4);
+ exit;
+}'
+}
+
+tmp=$(mktemp 'sfeed_curses_XXXXXXXXXX')
+trap "rm $tmp" EXIT
+cat > "$tmp"
+
+(awk -F '\t' '
+{
+ print "Title: " $2;
+ if (length($7))
+ print "Author: " $7;
+ if (length($3))
+ print "Link: " $3;
+ if (length($8))
+ print "Enclosure: " $8;
+ print "";
+ exit;
+}' < "$tmp"
+
+contenttype=$(awk -F '\t' '{ print $5; exit }' < "$tmp")
+if test x"$contenttype" = x"plain"; then
+ content < "$tmp"
+else
+ (echo "<span>"; content < "$tmp";echo "</span>") | \
+ lynx -stdin -dump -underline_links -image_links
+# lynx -stdin -dump -underline_links -list_inline -image_links
+# webdump -a -l -r -w 79
+fi
+) | \
+less -R
diff --git a/sfeed_curses.1 b/sfeed_curses.1
@@ -0,0 +1,122 @@
+.Dd June 20, 2020
+.Dt SFEED_CURSES 1
+.Os
+.Sh NAME
+.Nm sfeed_curses
+.Nd curses UI for viewing items
+.Sh SYNOPSIS
+.Nm
+.Op Ar file...
+.Sh DESCRIPTION
+.Nm
+formats feed data (TSV) from
+.Xr sfeed 1
+from stdin or
+.Ar file
+into a UI.
+If one or more
+.Ar file
+are specified, the basename of the
+.Ar file
+is used as the feed name in the output.
+If no
+.Ar file
+parameters are specified and so the data is read from stdin 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.
+.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 j, arrow up
+Select previous item.
+.It k, arrow down
+Select next item.
+.It h, arrow left
+Focus pane to the left.
+.It l, arrow right
+Focus pane to the right.
+.It TAB
+Cycle focused pane (feeds, items).
+.It g
+Go to first item.
+.It G
+Go to last item.
+.It page up, ^B
+Scroll 1 page up.
+.It page down, ^F, SPACE
+Scroll 1 page down.
+.It /
+Prompt for new search and search forward.
+.It ?
+Prompt for new search and search backward.
+.It n
+Search forward with the previous input search term.
+.It N
+Search backward with the previous input search term.
+.It r, ^L
+Redraw screen.
+.It R
+Reload all feed files as specified on startup when not read from stdin.
+.It m
+Toggle mouse mode.
+.It s
+Toggle showing the feeds pane sidebar.
+.It t
+Toggle showing only feeds with new items in the sidebar.
+.It a, e, @
+Plumb url of attachment/enclosure.
+The url is passed as a parameter to the program specified in
+.Ev SFEED_PLUMBER .
+.It o, newline
+Feeds pane: load feed and its items.
+Items pane: plumb current item url.
+The url is passed as a parameter to the program specified in
+.Ev SFEED_PLUMBER .
+.It p, |
+Pipe TAB-Separated Value line to a program.
+The line is piped to the program specified in
+.Ev SFEED_PIPER .
+.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: load feed and it's items.
+Items pane: select item, when already selected plumb it.
+.It right-click
+Items pane: plumb clicked item.
+.It scroll up
+Scroll 1 page up.
+.It scroll down
+Scroll 1 page down.
+.El
+.Sh ENVIRONMENT VARIABLES
+.Bl -tag -width Ds
+.It Ev SFEED_PIPER
+A program where the TAB-Separated Value line is piped to.
+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".
+.El
+.Sh EXIT STATUS
+.Ex -std
+.Sh SEE ALSO
+.Xr sfeed 1 ,
+.Xr sfeed_html 1 ,
+.Xr sfeed 5
+.Sh AUTHORS
+.An Hiltjo Posthuma Aq Mt hiltjo@codemadness.org
diff --git a/sfeed_curses.c b/sfeed_curses.c
@@ -0,0 +1,1467 @@
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <ctype.h>
+#include <curses.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <locale.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <term.h>
+#include <termios.h>
+#include <time.h>
+#include <unistd.h>
+#include <wchar.h>
+
+#define LEN(a) sizeof((a))/sizeof((a)[0])
+
+enum {
+ FieldUnixTimestamp = 0, FieldTitle, FieldLink, FieldContent,
+ FieldContentType, FieldId, FieldAuthor, FieldEnclosure, FieldLast
+};
+
+enum Pane { PaneFeeds, PaneItems, PaneLast };
+
+struct win {
+ int width;
+ int height;
+ int dirty;
+};
+
+struct row {
+ char *text;
+ int bold;
+ void *data;
+};
+
+struct scrollbar {
+ int tickpos;
+ int ticksize;
+ int x; /* absolute x position of the window on the screen */
+ int y; /* absolute y position of the window on the screen */
+ int size; /* absolute size of the bar */
+ int focused; /* has focus or not */
+ int hidden; /* is visible or not */
+ int dirty; /* needs draw update */
+};
+
+struct pane {
+ int x; /* absolute x position of the window on the screen */
+ int y; /* absolute y position of the window on the screen */
+ int width; /* absolute width of the window */
+ int height; /* absolute height of the window */
+ int pos; /* focused row position */
+ struct row *rows;
+ size_t nrows; /* total amount of rows */
+ off_t selected; /* row selected */ // TODO: factor out.
+ int focused; /* has focus or not */
+ int hidden; /* is visible or not */
+ int dirty; /* needs draw update */
+};
+
+struct statusbar {
+ int x; /* absolute x position of the window on the screen */
+ int y; /* absolute y position of the window on the screen */
+ int width; /* absolute width of the bar */
+ char *text; /* data */
+ int hidden; /* is visible or not */
+ int dirty; /* needs draw update */
+};
+
+#undef err
+void err(int, const char *, ...);
+#undef errx
+void errx(int, const char *, ...);
+
+void alldirty(void);
+void cleanup(void);
+void draw(void);
+void pane_draw(struct pane *);
+void pane_setpos(struct pane *p, off_t pos);
+void statusbar_draw(struct statusbar *);
+void sighandler(int);
+void updategeom(void);
+void updatesidebar(int);
+
+static struct statusbar statusbar;
+static struct pane panes[PaneLast];
+static struct scrollbar scrollbars[PaneLast]; /* each pane has a scrollbar */
+static struct win win;
+static int selpane;
+
+static struct termios tsave; /* terminal state at startup */
+static struct termios tcur;
+
+static struct winsize winsz; /* window size information */
+static int ttyfd = 0; /* fd of tty */
+static int devnullfd;
+
+static int needcleanup;
+
+/* feed info */
+struct feed {
+ char *name; /* feed name */
+ char *path; /* path to feed */
+ unsigned long totalnew; /* amount of new items per feed */
+ unsigned long total; /* total items */
+};
+
+static struct feed *feeds;
+static size_t nfeeds; /* amount of feeds */
+
+static time_t comparetime;
+static unsigned long totalnew, totalcount;
+
+struct item {
+ char *title;
+ char *url;
+ char *enclosure;
+ char *line;
+ int isnew;
+};
+
+/* config */
+int usemouse = 1; /* use xterm mouse tracking */
+int onlynew = 0; /* show only new in sidebar (default = 0 = all) */
+
+static char *plumber = "xdg-open";
+static char *piper = "less";
+
+void
+err(int code, const char *fmt, ...)
+{
+ va_list ap;
+ int saved_errno;
+
+ saved_errno = errno;
+ cleanup();
+
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+
+ fputs(": ", stderr);
+ errno = saved_errno;
+ perror(NULL);
+
+ _exit(code);
+}
+
+void
+errx(int code, const char *fmt, ...)
+{
+ va_list ap;
+
+ cleanup();
+
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+
+ fputc('\n', stderr);
+
+ _exit(code);
+}
+
+void *
+ecalloc(size_t nmemb, size_t size)
+{
+ void *p;
+
+ if (!(p = calloc(nmemb, size)))
+ err(1, "calloc");
+ return p;
+}
+
+char *
+estrdup(const char *s)
+{
+ char *p;
+
+ if (!(p = strdup(s)))
+ err(1, "strdup");
+ return p;
+}
+
+#undef strcasestr
+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;
+}
+
+/* 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: assumes time_t is 64-bit on 64-bit platforms:
+ long long (atleast 32-bit) to time_t. */
+ if (t)
+ *t = (time_t)l;
+
+ return 0;
+}
+
+int
+colw(const char *s)
+{
+ wchar_t wc;
+ size_t col = 0, i, slen;
+ int rl, w;
+
+ slen = strlen(s);
+ for (i = 0; i < slen; i += rl) {
+ if ((rl = mbtowc(&wc, &s[i], slen - i < 4 ? slen - i : 4)) <= 0)
+ break;
+ if ((w = wcwidth(wc)) == -1)
+ continue;
+ col += w;
+ }
+ return col;
+}
+
+/* format `len' columns of characters. If string is shorter pad the rest
+ * with characters `pad`. */
+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 rl, w;
+
+ if (!len)
+ return -1;
+
+ slen = strlen(s);
+ for (i = 0; i < slen; i += rl) {
+ if ((rl = mbtowc(&wc, &s[i], slen - i < 4 ? slen - i : 4)) <= 0)
+ break;
+ if ((w = wcwidth(wc)) == -1)
+ continue;
+ if (col + w > len || (col + w == len && s[i + rl])) {
+ if (siz + 4 >= bufsiz)
+ return -1;
+ memcpy(&buf[siz], "\xe2\x80\xa6", 3); /* ellipsis */
+ siz += 3;
+ if (col + w == len && w > 1)
+ buf[siz++] = pad;
+ buf[siz] = '\0';
+ return 0;
+ }
+ if (siz + rl + 1 >= bufsiz)
+ return -1;
+ memcpy(&buf[siz], &s[i], rl);
+ col += w;
+ siz += rl;
+ buf[siz] = '\0';
+ }
+
+ len -= col;
+ if (siz + len + 1 >= bufsiz)
+ return -1;
+ memset(&buf[siz], pad, len);
+ siz += len;
+ buf[siz] = '\0';
+
+ return 0;
+}
+
+void
+printpad(const char *s, int width)
+{
+ char buf[256]; // TODO: size
+ utf8pad(buf, sizeof(buf), s, width, ' ');
+ fputs(buf, stdout);
+}
+
+void
+resettitle(void)
+{
+ printf("\x1b%c", 'c'); /* reset title and state */
+}
+
+void
+updatetitle(void)
+{
+ printf("\x1b]2;(%lu/%lu) - sfeed_curses\x1b\\", totalnew, totalcount);
+}
+
+void
+mousemode(int on)
+{
+ putp(on ? "\x1b[?1000h" : "\x1b[?1000l");
+}
+
+void
+cleanup(void)
+{
+ if (!needcleanup)
+ return;
+
+ /* restore terminal settings */
+ tcsetattr(ttyfd, TCSANOW, &tsave);
+
+ putp(tparm(cursor_visible, 0, 0, 0, 0, 0, 0, 0, 0, 0));
+ putp(tparm(clear_screen, 0, 0, 0, 0, 0, 0, 0, 0, 0));
+
+ /* xterm mouse-mode */
+ if (usemouse)
+ mousemode(0);
+
+ resettitle();
+ fflush(stdout);
+
+ signal(SIGWINCH, SIG_DFL);
+
+ needcleanup = 0;
+}
+
+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;
+}
+
+void
+getwinsize(void)
+{
+ if (ioctl(ttyfd, TIOCGWINSZ, &winsz) == -1)
+ err(1, "ioctl");
+ win_update(&win, winsz.ws_col, winsz.ws_row);
+ if (win.dirty)
+ alldirty();
+}
+
+void
+init(void)
+{
+ tcgetattr(ttyfd, &tsave);
+ memcpy(&tcur, &tsave, sizeof(tcur));
+ tcur.c_lflag &= ~(ECHO|ICANON);
+ tcsetattr(ttyfd, TCSANOW, &tcur);
+
+ getwinsize();
+
+ setupterm(NULL, 1, NULL);
+ putp(tparm(save_cursor, 0, 0, 0, 0, 0, 0, 0, 0, 0));
+ putp(tparm(cursor_invisible, 0, 0, 0, 0, 0, 0, 0, 0, 0));
+
+ /* xterm mouse-mode */
+ if (usemouse)
+ mousemode(usemouse);
+
+ updategeom();
+
+ fflush(stdout);
+
+ signal(SIGWINCH, sighandler);
+
+ needcleanup = 1;
+}
+
+void
+pipeitem(const char *line)
+{
+ FILE *fp;
+ int pid, wpid;
+
+ cleanup();
+ switch ((pid = fork())) {
+ case -1:
+ err(1, "fork");
+ return;
+ case 0:
+ errno = 0;
+ if (!(fp = popen(piper, "we"))) {
+ fputs("popen: ", stderr);
+ perror(NULL);
+ _exit(1);
+ }
+ fputs(line, fp);
+ fputc('\n', fp);
+ exit(pclose(fp));
+ break;
+ default:
+ while ((wpid = wait(NULL)) >= 0 && wpid != pid)
+ ;
+ }
+ updatesidebar(onlynew);
+ updatetitle();
+ init();
+}
+
+void
+plumb(char *url)
+{
+ switch (fork()) {
+ case -1:
+ err(1, "fork");
+ return;
+ case 0:
+ dup2(devnullfd, 1);
+ dup2(devnullfd, 2);
+ if (execlp(plumber, plumber, url, NULL) < 0)
+ _exit(1);
+ }
+}
+
+struct row *
+pane_row_get(struct pane *p, off_t pos)
+{
+ return p->rows + pos;
+}
+
+void
+pane_row_draw(struct pane *p, off_t pos)
+{
+ int r = 0, y;
+
+ y = p->y + (pos % p->height); /* relative position on screen */
+ putp(tparm(cursor_address, y, p->x, 0, 0, 0, 0, 0, 0, 0));
+
+ if (pos == p->pos) {
+ if (p->focused)
+ putp("\x1b[7m"); /* standout */
+ else
+ putp("\x1b[2;7m"); /* gray, standout */
+ r = 1;
+ } else if (p->nrows && pos < p->nrows && p->rows[pos].bold) {
+ putp("\x1b[1m"); /* bold */
+ r = 1;
+ }
+ printpad(p->rows[pos].text, p->width);
+ if (r)
+ putp("\x1b[0m");
+}
+
+void
+pane_setpos(struct pane *p, off_t pos)
+{
+ off_t prev;
+
+ if (pos == p->pos)
+ return; /* no change */
+ if (!p->nrows)
+ return; /* invalid */
+ if (pos < 0)
+ pos = 0; /* clamp */
+ if (pos >= p->nrows)
+ pos = p->nrows - 1; /* clamp */
+
+ /* is on different screen ? */
+ if (((p->pos - (p->pos % p->height)) / p->height) !=
+ ((pos - (pos % p->height)) / p->height)) {
+ p->pos = pos;
+ // TODO: dirty region handling? or setting dirty per row?
+ p->dirty = 1;
+ pane_draw(p);
+ } else {
+ prev = p->pos;
+ p->pos = pos;
+ // TODO: dirty region handling? or setting dirty per row?
+ pane_row_draw(p, prev); /* draw previous row again */
+ pane_row_draw(p, p->pos); /* draw new highlighted row */
+ }
+}
+
+void
+pane_setstartpos(struct pane *p)
+{
+ pane_setpos(p, 0);
+}
+
+void
+pane_setendpos(struct pane *p)
+{
+ pane_setpos(p, p->nrows);
+}
+
+void
+pane_scrollpage(struct pane *p, size_t 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);
+ }
+}
+
+void
+pane_scrolln(struct pane *p, int n)
+{
+ if (n < 0)
+ pane_setpos(p, p->pos + n);
+ else if (n > 0)
+ pane_setpos(p, p->pos + n);
+}
+
+void
+pane_setfocus(struct pane *p, int on)
+{
+ if (p->focused != on) {
+ p->focused = on;
+ p->dirty = 1;
+ }
+}
+
+void
+pane_draw(struct pane *p)
+{
+ off_t pos, start, end;
+ int y, h;
+
+ if (p->hidden || !p->dirty)
+ return;
+
+ h = p->pos % p->height;
+ start = p->pos - h;
+ end = p->pos - h + p->height;
+ if (end >= p->nrows)
+ end = p->nrows;
+
+ for (pos = start, y = 0; pos < end && y < p->height; pos++, y++)
+ pane_row_draw(p, pos);
+
+ /* rest: empty space */
+ for (; y < p->height; y++) {
+ putp(tparm(cursor_address, p->y + y, p->x, 0, 0, 0, 0, 0, 0, 0));
+ printf("%-*.*s", p->width, p->width, "");
+ }
+ p->dirty = 0;
+}
+
+/* cycle visible pane in a direction, but don't cycle back */
+void
+cyclepanen(int n)
+{
+ int i;
+
+ if (n < 0) {
+ n = -n; /* absolute */
+ for (i = selpane; n && i - 1 >= 0; i--) {
+ if (panes[i - 1].hidden)
+ continue;
+ n--;
+ selpane = i - 1;
+ }
+ } else if (n > 0) {
+ for (i = selpane; n && i + 1 < LEN(panes); i++) {
+ if (panes[i + 1].hidden)
+ continue;
+ n--;
+ selpane = i + 1;
+ }
+ }
+}
+
+/* cycle visible panes */
+void
+cyclepane(void)
+{
+ int i;
+
+ i = selpane;
+ cyclepanen(+1);
+ /* reached end, cycle back to first most-visible */
+ if (i == selpane)
+ cyclepanen(-PaneLast);
+}
+
+void
+updategeom(void)
+{
+ int w, x;
+
+ panes[PaneFeeds].x = 0;
+ panes[PaneFeeds].y = 0;
+ panes[PaneFeeds].height = win.height - 1; /* space for statusbar */
+
+ /* NOTE: updatesidebar() must happen before updategeometry for the
+ remaining width */
+ if (!panes[PaneFeeds].hidden) {
+ w = win.width - panes[PaneFeeds].width;
+ x = panes[PaneFeeds].x + panes[PaneFeeds].width;
+ /* space for scrollbar if sidebar is visible */
+ w--;
+ x++;
+ } else {
+ w = win.width;
+ x = 0;
+ }
+
+ panes[PaneItems].width = w - 1; /* rest and space for scrollbar */
+ panes[PaneItems].height = win.height - 1; /* space for statusbar */
+ panes[PaneItems].x = x;
+ panes[PaneItems].y = 0;
+
+ 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;
+
+ /* statusbar below */
+ statusbar.width = win.width;
+ statusbar.x = 0;
+ statusbar.y = win.height - 1;
+
+ alldirty();
+}
+
+void
+scrollbar_setfocus(struct scrollbar *s, int on)
+{
+ if (s->focused != on) {
+ s->focused = on;
+ s->dirty = 1;
+ }
+}
+
+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;
+}
+
+void
+scrollbar_draw(struct scrollbar *s)
+{
+ off_t y;
+
+ if (s->hidden || !s->dirty)
+ return;
+
+ if (!s->focused)
+ putp("\x1b[2m");
+ for (y = 0; y < s->size; y++) {
+ putp(tparm(cursor_address, s->y + y, s->x, 0, 0, 0, 0, 0, 0, 0));
+ if (y >= s->tickpos && y < s->tickpos + s->ticksize) {
+ putp(tparm(enter_standout_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
+ fputs(" ", stdout);
+ putp(tparm(exit_standout_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
+ } else {
+ fputs("\xe2\x94\x82", stdout); /* symbol: "light vertical" */
+ }
+ }
+ if (!s->focused)
+ putp("\x1b[0m");
+ s->dirty = 0;
+}
+
+char *
+uiprompt(int x, int y, char *fmt, ...)
+{
+ va_list ap;
+ char *input = NULL;
+ size_t n;
+ ssize_t r;
+ char buf[256]; // TODO
+ struct termios tset;
+
+ va_start(ap, fmt);
+ if (vsnprintf(buf, sizeof(buf), fmt, ap) >= sizeof(buf))
+ buf[sizeof(buf) - 1] = '\0';
+ va_end(ap);
+
+ tcgetattr(ttyfd, &tset);
+ tset.c_lflag |= (ECHO|ICANON);
+ tcsetattr(ttyfd, TCSANOW, &tset);
+
+ putp(tparm(save_cursor, 0, 0, 0, 0, 0, 0, 0, 0, 0));
+ putp(tparm(cursor_address, y, x, 0, 0, 0, 0, 0, 0, 0));
+ putp(tparm(enter_standout_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
+ fputs(buf, stdout);
+ putp(tparm(exit_standout_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
+
+ putp(tparm(cursor_visible, 0, 0, 0, 0, 0, 0, 0, 0, 0));
+ putp(tparm(cursor_address, y, x + colw(buf), 0, 0, 0, 0, 0, 0, 0));
+ putp(tparm(clr_eol, 0, 0, 0, 0, 0, 0, 0, 0, 0));
+ fflush(stdout);
+
+ n = 0;
+ r = getline(&input, &n, stdin);
+
+ putp(tparm(cursor_invisible, 0, 0, 0, 0, 0, 0, 0, 0, 0));
+ tcsetattr(ttyfd, TCSANOW, &tcur); /* restore */
+ putp(tparm(restore_cursor, 0, 0, 0, 0, 0, 0, 0, 0, 0));
+
+ fflush(stdout);
+
+ if (r < 0) {
+ clearerr(stdin);
+ free(input);
+ input = NULL;
+ } else if (input[r - 1] == '\n') {
+ input[--r] = '\0';
+ }
+
+ return input;
+}
+
+void
+statusbar_draw(struct statusbar *s)
+{
+ if (s->hidden || !s->dirty)
+ return;
+ putp(tparm(cursor_address, s->y, s->x, 0, 0, 0, 0, 0, 0, 0));
+ putp(tparm(enter_standout_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
+ printpad(s->text, s->width);
+ putp(tparm(exit_standout_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0));
+ s->dirty = 0;
+}
+
+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;
+}
+
+int
+linetoitem(char *line, struct item *item)
+{
+ char *fields[FieldLast];
+ char title[1024]; // TODO
+ struct tm *tm;
+ time_t parsedtime;
+
+ item->line = estrdup(line);
+
+ parseline(line, fields);
+
+ parsedtime = 0;
+ if (strtotime(fields[FieldUnixTimestamp], &parsedtime)) {
+ free(item->line);
+ item->line = NULL;
+ return -1;
+ }
+ if (!(tm = localtime(&parsedtime))) {
+ free(item->line);
+ item->line = NULL;
+ return -1;
+ }
+
+ // TODO: asnprintf?
+ snprintf(title, sizeof(title), "%c %04d-%02d-%02d %02d:%02d %s",
+ fields[FieldEnclosure][0] ? '@' : ' ',
+ tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
+ tm->tm_hour, tm->tm_min, fields[FieldTitle]);
+
+ item->title = estrdup(title);
+ item->url = estrdup(fields[FieldLink]);
+ item->enclosure = estrdup(fields[FieldEnclosure]);
+ item->isnew = (parsedtime >= comparetime);
+
+ return 0;
+}
+
+int
+feed_getitems(struct item **items, size_t *nitems, ssize_t want,
+ FILE *fp, size_t offset)
+{
+ struct item *item;
+ char *line = NULL;
+ size_t cap, linesize = 0;
+ ssize_t i, linelen;
+ int ret = -1;
+
+ *items = NULL;
+ *nitems = 0;
+ if (fseek(fp, offset, SEEK_SET))
+ goto err;
+
+ /* if loading all items, set cap to 0, expand later */
+ if (want == -1) {
+ cap = 0;
+ } else { /* `want` is also a hint of amount to allocate. */
+ cap = (size_t)want;
+ *items = realloc(*items, cap * sizeof(struct item));
+ }
+ for (i = 0; want == -1 || i < want; i++) {
+ if (i + 1 >= cap)
+ *items = realloc(*items, ++cap * sizeof(struct item));
+ if ((linelen = getline(&line, &linesize, fp)) > 0) {
+ item = (*items) + i;
+ offset += linelen;
+
+ if (line[linelen - 1] == '\n')
+ line[--linelen] = '\0';
+
+ if (linetoitem(line, item) == -1)
+ errx(1, "invalid line");
+
+ (*nitems)++;
+ }
+ if (ferror(fp))
+ goto err;
+ if (linelen <= 0 || feof(fp))
+ break;
+ }
+ ret = 0;
+
+err:
+ free(line);
+ return ret;
+}
+
+void
+feed_load(struct feed *f, FILE *fp)
+{
+ static struct item *items = NULL;
+ static size_t nitems = 0;
+ struct pane *p;
+ struct row *row;
+ ssize_t want;
+ size_t i;
+ int ret;
+
+ for (i = 0; i < nitems; i++) {
+ free(items[i].title);
+ free(items[i].url);
+ free(items[i].enclosure);
+ free(items[i].line);
+ }
+ free(items);
+ items = NULL;
+ nitems = 0;
+
+ p = &panes[PaneItems];
+ for (i = 0; i < p->nrows; i++)
+ free(p->rows[i].text);
+ free(p->rows);
+ p->rows = NULL;
+ p->nrows = 0;
+
+ want = -1; /* all */
+
+ ret = feed_getitems(&items, &nitems, want, fp, 0);
+ if (ret == -1)
+ err(1, "%s: %s", __func__, f->path);
+
+ f->totalnew = 0;
+ f->total = nitems;
+ for (i = 0; i < nitems; i++)
+ f->totalnew += items[i].isnew;
+
+ p->rows = ecalloc(sizeof(p->rows[0]), nitems + 1);
+ for (i = 0; i < nitems; i++) {
+ row = pane_row_get(p, i);
+ row->text = estrdup(items[i].title);
+ row->bold = items[i].isnew;
+ row->data = &items[i];
+ }
+ p->nrows = nitems;
+ pane_setpos(p, 0);
+ p->selected = 0;
+}
+
+void
+feed_loadfile(struct feed *f, const char *path)
+{
+ FILE *fp;
+
+ if (!(fp = fopen(path, "r")))
+ err(1, "fopen: %s", path);
+ feed_load(f, fp);
+ if (ferror(fp))
+ err(1, "ferror: %s", path);
+ fclose(fp);
+}
+
+void
+feed_count(struct feed *f, FILE *fpin)
+{
+ char *fields[FieldLast];
+ char *line = NULL;
+ size_t linesize = 0;
+ ssize_t linelen;
+ struct tm *tm;
+ time_t parsedtime;
+
+ while ((linelen = getline(&line, &linesize, fpin)) > 0) {
+ if (line[linelen - 1] == '\n')
+ line[--linelen] = '\0';
+ parseline(line, fields);
+
+ parsedtime = 0;
+ if (strtotime(fields[FieldUnixTimestamp], &parsedtime))
+ continue;
+ if (!(tm = localtime(&parsedtime)))
+ continue; /* ignore item */
+
+ f->totalnew += (parsedtime >= comparetime);
+ f->total++;
+ }
+ free(line);
+}
+
+void
+loadfiles(int argc, char *argv[])
+{
+ FILE *fp;
+ size_t i;
+ char *name;
+
+ feeds = ecalloc(argc, sizeof(struct feed));
+
+ if ((comparetime = time(NULL)) == -1)
+ err(1, "time");
+ /* 1 day is old news */
+ comparetime -= 86400;
+
+ if ((devnullfd = open("/dev/null", O_WRONLY)) < 0)
+ err(1, "open: /dev/null");
+
+ totalnew = totalcount = 0;
+ if (argc == 1) {
+ feeds[0].name = "stdin";
+
+ if (!(fp = fdopen(ttyfd, "rb")))
+ err(1, "fdopen");
+ feed_load(&feeds[0], fp);
+
+ /* close and re-attach to stdin */
+ close(0); // TODO: fclose ?
+ if ((ttyfd = open("/dev/tty", O_RDONLY)) == -1)
+ err(1, "open: /dev/tty");
+ if (dup2(ttyfd, 0) == -1)
+ err(1, "dup2: /dev/tty");
+
+ nfeeds = 1;
+ totalnew += feeds[0].totalnew;
+ totalcount += feeds[0].total;
+ } else if (argc > 1) {
+ for (i = 1; i < argc; i++) {
+ feeds[i - 1].path = argv[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]);
+ feed_count(&feeds[i - 1], fp);
+ if (ferror(fp))
+ err(1, "ferror: %s", argv[i]);
+ fclose(fp);
+
+ totalnew += feeds[i - 1].totalnew;
+ totalcount += feeds[i - 1].total;
+ }
+ nfeeds = argc - 1;
+ /* load first items, because of first selection. */
+ feed_loadfile(&feeds[0], feeds[0].path);
+ }
+}
+
+void
+updatesidebar(int onlynew)
+{
+ struct pane *p;
+ size_t i, r;
+ int feedw, len;
+ char counts[128]; // TODO: size
+ char bufw[256]; // TODO: size
+ char tmp[256];
+
+ p = &panes[PaneFeeds];
+ for (i = 0; i < p->nrows; i++)
+ free(p->rows[i].text);
+ free(p->rows);
+ p->rows = NULL;
+
+ p->nrows = 0;
+ for (i = 0; i < nfeeds; i++) {
+ if (onlynew && feeds[i].totalnew == 0)
+ continue;
+ p->nrows++;
+ }
+ p->rows = ecalloc(sizeof(p->rows[0]), p->nrows + 1);
+
+ feedw = 0;
+ for (i = 0; i < nfeeds; i++) {
+ if (onlynew && feeds[i].totalnew == 0)
+ continue;
+ snprintf(bufw, sizeof(bufw), "%s (%ld/%ld)",
+ feeds[i].name, feeds[i].totalnew, feeds[i].total);
+ len = colw(bufw);
+ if (len > feedw)
+ feedw = len;
+ }
+
+ for (i = 0, r = 0; i < nfeeds; i++) {
+ if (onlynew && feeds[i].totalnew == 0)
+ continue;
+ len = snprintf(counts, sizeof(counts), "(%ld/%ld)",
+ feeds[i].totalnew, feeds[i].total);
+
+ utf8pad(bufw, sizeof(bufw), feeds[i].name, feedw - len, ' ');
+ snprintf(tmp, sizeof(tmp), "%s%s", bufw, counts);
+
+ p->rows[r].text = estrdup(tmp);
+ p->rows[r].bold = (feeds[i].totalnew > 0);
+ p->rows[r].data = &feeds[i];
+
+ r++;
+ }
+
+ panes[PaneFeeds].width = feedw;
+}
+
+void
+sighandler(int signo)
+{
+ if (signo == SIGWINCH) {
+ getwinsize();
+ updategeom();
+ draw();
+ }
+}
+
+void
+alldirty(void)
+{
+ win.dirty = 1;
+ panes[PaneFeeds].dirty = 1;
+ panes[PaneItems].dirty = 1;
+ scrollbars[PaneFeeds].dirty = 1;
+ scrollbars[PaneItems].dirty = 1;
+ statusbar.dirty = 1;
+}
+
+void
+draw(void)
+{
+ struct row *row;
+ struct item *item;
+ size_t i;
+
+ if (win.width <= 1 || win.height <= 3 ||
+ (!panes[PaneFeeds].hidden && win.width <= panes[PaneFeeds].width + 2))
+ return;
+
+ if (win.dirty) {
+ putp(tparm(clear_screen, 0, 0, 0, 0, 0, 0, 0, 0, 0));
+ win.dirty = 0;
+ }
+
+ /* if item selection text changed, update the status text */
+ if ((row = pane_row_get(&panes[PaneItems], panes[PaneItems].pos))) {
+ item = (struct item *)row->data;
+ statusbar_update(&statusbar, item->url);
+ } else {
+ statusbar_update(&statusbar, "");
+ }
+
+ /* NOTE: theres the same amount and indices of panes and scrollbars. */
+ for (i = 0; i < LEN(panes); i++) {
+ pane_setfocus(&panes[i], i == selpane);
+ pane_draw(&panes[i]);
+
+ 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]);
+ }
+ statusbar_draw(&statusbar);
+ fflush(stdout);
+}
+
+void
+mousereport(int button, int release, int x, int y)
+{
+ struct pane *p;
+ struct feed *f;
+ struct row *row;
+ struct item *item;
+ size_t i;
+ int changedpane, pos;
+
+ if (!usemouse || release || button == -1)
+ return;
+
+ for (i = 0; i < LEN(panes); i++) {
+ p = &panes[i];
+ if (p->hidden)
+ continue;
+
+ if (!(x >= p->x && x < p->x + p->width &&
+ 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);
+
+ switch (button) {
+ case 0: /* left-click */
+ if (!p->nrows || pos >= p->nrows)
+ break;
+ if (i == PaneFeeds) {
+ pane_setpos(p, pos);
+ p->selected = pos;
+ row = pane_row_get(&panes[PaneFeeds], pos);
+ f = (struct feed *)row->data;
+ feed_loadfile(f, f->path);
+ panes[PaneItems].dirty = 1;
+ scrollbars[PaneItems].dirty = 1;
+ } else if (i == PaneItems) {
+ /* clicking the same highlighted row */
+ if (p->pos == pos && !changedpane) {
+ row = pane_row_get(&panes[PaneItems], pos);
+ item = (struct item *)row->data;
+ plumb(item->url);
+ } else {
+ pane_setpos(p, pos);
+ }
+ }
+ break;
+ case 2: /* right-click */
+ if (!p->nrows || pos >= p->nrows)
+ break;
+ if (i == PaneItems) {
+ pane_setpos(p, pos);
+ p = &panes[PaneFeeds];
+ f = (struct feed *)p->rows[p->selected].data;
+ p = &panes[PaneItems];
+ item = (struct item *)p->rows[p->pos].data;
+ pipeitem(item->line);
+ }
+ break;
+ case 3: /* scroll up */
+ case 4: /* scroll down */
+ pane_scrollpage(p, button == 3 ? -1 : +1);
+ break;
+ }
+ draw();
+ }
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct pane *p;
+ struct feed *f;
+ struct row *row;
+ struct item *item;
+ char *tmp;
+ char *search = NULL; /* search text */
+ int ch, button, x, y, release; /* mouse button event */
+ off_t off;
+
+ setlocale(LC_CTYPE, "");
+
+ if ((tmp = getenv("SFEED_PLUMBER")))
+ plumber = tmp;
+ if ((tmp = getenv("SFEED_PIPER")))
+ piper = tmp;
+
+ loadfiles(argc, argv);
+
+ if (argc > 1) {
+ panes[PaneFeeds].hidden = 0;
+ selpane = PaneFeeds;
+ } else {
+ panes[PaneFeeds].hidden = 1;
+ selpane = PaneItems;
+ }
+
+ updatetitle();
+ updatesidebar(onlynew);
+ init();
+
+ atexit(cleanup);
+
+ draw();
+
+ while ((ch = getchar()) != EOF) {
+ switch (ch) {
+ case '\x1b':
+ if ((ch = getchar()) != '[')
+ continue; /* unhandled */
+ switch ((ch = getchar())) {
+ case EOF: goto end;
+ case 'M': /* reported mouse event */
+ if ((ch = getchar()) == EOF)
+ goto end;
+ /* button numbers (0 - 2) encoded in lowest 2 bits
+ release does not indicate which button (so set to 0). */
+ ch -= 32;
+
+ /* extended buttons like scrollwheels */
+ release = 0;
+ if (ch >= 64) {
+ button = ((ch - 64) & 3) + 3;
+ } else {
+ button = ch & 3;
+ if (button == 3) {
+ button = -1;
+ release = 1;
+ }
+ }
+ /* X10 mouse-encoding */
+ if ((x = getchar()) == EOF)
+ goto end;
+ if ((y = getchar()) == EOF)
+ goto end;
+ mousereport(button, release, x - 33, y - 33);
+ break;
+ case 'A': goto keyup; /* arrow up */
+ case 'B': goto keydown; /* arrow down */
+ case 'C': goto keyright; /* arrow left */
+ case 'D': goto keyleft; /* arrow right */
+ case 'H': goto startpos; /* home */
+ case '4': /* end */
+ if ((ch = getchar()) == '~')
+ goto endpos;
+ continue;
+ case '5': /* page up */
+ if ((ch = getchar()) == '~')
+ goto prevpage;
+ continue;
+ case '6': /* page down */
+ if ((ch = getchar()) == '~')
+ goto nextpage;
+ continue;
+ }
+ continue;
+keyup:
+ case 'j':
+ pane_scrolln(&panes[selpane], -1);
+ break;
+keydown:
+ case 'k':
+ pane_scrolln(&panes[selpane], +1);
+ break;
+keyleft:
+ case 'h':
+ cyclepanen(-1);
+ break;
+keyright:
+ case 'l':
+ cyclepanen(+1);
+ break;
+ case '\t':
+ cyclepane();
+ break;
+startpos:
+ case 'g':
+ pane_setstartpos(&panes[selpane]);
+ break;
+endpos:
+ case 'G':
+ pane_setendpos(&panes[selpane]);
+ break;
+prevpage:
+ case 2: /* ^B */
+ pane_scrollpage(&panes[selpane], -1);
+ break;
+nextpage:
+ case ' ':
+ case 6: /* ^F */
+ pane_scrollpage(&panes[selpane], +1);
+ break;
+ case '/': /* new search (forward) */
+ case '?': /* new search (backward) */
+ case 'n': /* search again (forward) */
+ case 'N': /* search again (backward) */
+ p = &panes[selpane];
+ if (!p->nrows)
+ break;
+
+ /* prompt for new input */
+ if (ch == '?' || ch == '/') {
+ tmp = ch == '?' ? "backward" : "forward";
+ free(search);
+ search = uiprompt(statusbar.x, statusbar.y,
+ "Search (%s): ", tmp);
+ alldirty();
+ draw();
+ }
+ if (!search)
+ break;
+
+ if (ch == '/' || ch == 'n') {
+ /* forward */
+ for (off = p->pos + 1; off < p->nrows; off++) {
+ if (strcasestr(p->rows[off].text, search)) {
+ pane_setpos(p, off);
+ break;
+ }
+ }
+ } else {
+ /* backward */
+ for (off = p->pos - 1; off >= 0; off--) {
+ if (strcasestr(p->rows[off].text, search)) {
+ pane_setpos(p, off);
+ break;
+ }
+ }
+
+ }
+ break;
+ case 12: /* ^L */
+ case 'r': /* update window dimensions and redraw */
+ alldirty();
+ break;
+ case 'R': /* reload all files */
+ /* if read from stdin then don't reload items. */
+ if (argc <= 1)
+ break;
+ loadfiles(argc, argv);
+ panes[PaneFeeds].pos = 0;
+ panes[PaneFeeds].selected = 0;
+ updatesidebar(onlynew);
+ updategeom();
+ updatetitle();
+ break;
+ case 'a': /* attachment */
+ case 'e': /* enclosure */
+ case '@':
+ if (selpane == PaneItems && panes[PaneItems].nrows) {
+ p = &panes[PaneFeeds];
+ f = (struct feed *)p->rows[p->selected].data;
+ p = &panes[PaneItems];
+ item = (struct item *)p->rows[p->pos].data;
+ plumb(item->enclosure);
+ }
+ break;
+ case 'm': /* toggle mouse mode */
+ usemouse = !usemouse;
+ mousemode(usemouse);
+ fflush(stdout);
+ break;
+ case 's': /* toggle sidebar */
+ // TODO: mark as dirty.
+ panes[PaneFeeds].hidden = !panes[PaneFeeds].hidden;
+ if (selpane == PaneFeeds && panes[PaneFeeds].hidden)
+ selpane = PaneItems;
+ updategeom();
+ break;
+ case 't': /* toggle showing only new in sidebar */
+ onlynew = !onlynew;
+ pane_setpos(&panes[PaneFeeds], 0);
+ panes[PaneFeeds].selected = 0;
+ updatesidebar(onlynew);
+ updategeom();
+ break;
+ case 'o': /* feeds: load, items: plumb url */
+ case '\n':
+ if (selpane == PaneFeeds && panes[PaneFeeds].nrows) {
+ p = &panes[selpane];
+ p->selected = p->pos;
+ f = (struct feed *)p->rows[p->pos].data;
+ feed_loadfile(f, f->path);
+ pane_setpos(&panes[PaneItems], 0);
+ panes[PaneItems].dirty = 1;
+ scrollbars[PaneItems].dirty = 1;
+ } else if (selpane == PaneItems && panes[PaneItems].nrows) {
+ p = &panes[PaneItems];
+ row = pane_row_get(p, p->pos);
+ item = (struct item *)row->data;
+ plumb(item->url);
+ }
+ break;
+ case 'p': /* items: pipe TSV line to program */
+ case '|':
+ if (selpane == PaneItems && panes[PaneItems].nrows) {
+ p = &panes[PaneFeeds];
+ f = (struct feed *)p->rows[p->selected].data;
+ p = &panes[PaneItems];
+ row = pane_row_get(p, p->pos);
+ item = (struct item *)row->data;
+ pipeitem(item->line);
+ }
+ break;
+ case 4: /* EOT */
+ case 'q': goto end;
+ }
+ draw();
+ }
+end:
+ return 0;
+}